<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>개발수양록</title>
    <link>https://blog.benelog.net/</link>
    <atom:link href="https://blog.benelog.net//feed.xml" rel="self" type="application/rss+xml" />
    <description>S/W 개발에 관한 정상혁의 글 모음</description>
    <language>ko</language>
    <pubDate>Sat, 3 Oct 2026 00:32:56 +0000</pubDate>
    <lastBuildDate>Sat, 3 Oct 2026 00:32:56 +0000</lastBuildDate>

    <item>
      <title>Java에서 외부 프로세스를 실행하기: JDK 25와 Linux 6.x 기준</title>
      <link>https://blog.benelog.net//java-external-process.html</link>
      <pubDate>Thu, 3 Sep 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">java-external-process.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_기본_클래스로_외부_프로세스를_실행할_때의_주의점&quot;&gt;JDK 기본 클래스로 외부 프로세스를 실행할 때의 주의점&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#processbuilder와_runtime_exec의_차이&quot;&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt;와 &lt;code&gt;Runtime.exec&lt;/code&gt;의 차이&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#processbuilder_기본_옵션의_한계&quot;&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt; 기본 옵션의 한계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#표준_출력과_표준_오류의_파이프_처리&quot;&gt;표준 출력과 표준 오류의 파이프 처리&lt;/a&gt;
&lt;ul class=&quot;sectlevel3&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#읽지_않은_파이프_버퍼가_부르는_교착&quot;&gt;읽지 않은 파이프 버퍼가 부르는 교착&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#출력을_읽을_때는_두_스트림을_동시에_처리&quot;&gt;출력을 읽을 때는 두 스트림을 동시에 처리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#java_프로세스에서_읽지_않을_출력은_리다이렉트&quot;&gt;Java 프로세스에서 읽지 않을 출력은 리다이렉트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#표준_입력_닫기와_eof&quot;&gt;표준 입력 닫기와 EOF&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#시간제한과_종료_정책&quot;&gt;시간제한과 종료 정책&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#후손_프로세스의_정리&quot;&gt;후손 프로세스의 정리&lt;/a&gt;
&lt;ul class=&quot;sectlevel3&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#후손_프로세스가_남는_사례&quot;&gt;후손 프로세스가 남는 사례&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#프로세스_그룹_단위의_종료&quot;&gt;프로세스 그룹 단위의 종료&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#cgroup_단위의_종료&quot;&gt;cgroup 단위의 종료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#zt_exec과_apache_commons_exec의_활용&quot;&gt;zt-exec과 Apache Commons Exec의 활용&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#zt_exec&quot;&gt;zt-exec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#apache_commons_exec&quot;&gt;Apache Commons Exec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#유지보수_현황과_선택&quot;&gt;유지보수 현황과 선택&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#linux에서의_프로세스_생성과_운영&quot;&gt;Linux에서의 프로세스 생성과 운영&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#프로세스_생성_방식_세_가지와_jdk_기본값의_변천&quot;&gt;프로세스 생성 방식 세 가지와 JDK 기본값의 변천&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#posix_spawn의_동작&quot;&gt;POSIX_SPAWN의 동작&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#fork의_메모리_비용과_실행_지연&quot;&gt;FORK의 메모리 비용과 실행 지연&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jvm_업그레이드_때_정리할_vfork_설정&quot;&gt;JVM 업그레이드 때 정리할 VFORK 설정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#마치며&quot;&gt;마치며&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글은 JDK 25와 Linux 6.x를 기준으로 Java에서 외부 프로세스를 실행하는 코드를 작성할 때 주의해야 할 요소와 활용할 수 있는 라이브러리를 소개합니다.
이어서 Linux에서 JVM이 자식 프로세스를 만드는 방식인 &lt;code&gt;posix_spawn()&lt;/code&gt; 함수와 JDK에 포함된 작은 실행 파일인 &lt;code&gt;jspawnhelper&lt;/code&gt;의 동작, 큰 힙에서 나타나는 실행 지연, 실행 방식을 바꾸는 설정까지 살펴봅니다.
이 글의 실행 결과는 아래 환경에서 확인했습니다. 예제 코드는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/java-external-process&quot;&gt;GitHub 저장소&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 66.6667%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OS&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Ubuntu 24.04.4 LTS, 커널 6.17&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아키텍처&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;x86-64&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;glibc&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2.39&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Temurin 25+36&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;zt-exec&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.13.0&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Apache Commons Exec&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.6.0&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jdk_기본_클래스로_외부_프로세스를_실행할_때의_주의점&quot;&gt;JDK 기본 클래스로 외부 프로세스를 실행할 때의 주의점&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;processbuilder와_runtime_exec의_차이&quot;&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt;와 &lt;code&gt;Runtime.exec&lt;/code&gt;의 차이&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 코드에서 실행한 외부 프로세스는 &lt;code&gt;java.lang.Process&lt;/code&gt; 객체로 다룹니다. &lt;code&gt;Process&lt;/code&gt; 객체는 &lt;code&gt;ProcessBuilder.start()&lt;/code&gt;나 &lt;code&gt;Runtime.exec()&lt;/code&gt;로 얻습니다. &lt;code&gt;ProcessBuilder&lt;/code&gt;는 &lt;a href=&quot;https://docs.oracle.com/javase/1.5.0/docs/guide/lang/enhancements.html&quot;&gt;JDK 5.0(Java 1.5)에 추가&lt;/a&gt;되었고, Sun JDK의 &lt;code&gt;Runtime.exec()&lt;/code&gt;도 이때부터 내부에서 &lt;code&gt;ProcessBuilder&lt;/code&gt;에 위임하도록 바뀌었습니다. &lt;a href=&quot;https://www.javainthebox.net/laboratory/J2SE1.5/TinyTips/ProcessBuilder/ProcessBuilder.html&quot;&gt;JDK 1.5.0-beta2의 소스를 확인한 당시 자료&lt;/a&gt;에도 이 구현이 나옵니다. OpenJDK 25의 &lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/Runtime.java&quot;&gt;Runtime 소스&lt;/a&gt;에서도 &lt;code&gt;ProcessBuilder&lt;/code&gt;를 만들어 &lt;code&gt;start()&lt;/code&gt;를 호출합니다. 따라서 두 API는 프로세스 생성 구현을 공유하며, 주요 차이는 실행 설정을 다루는 방법입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;새로 작성하는 코드에는 &lt;code&gt;ProcessBuilder&lt;/code&gt;를 권장합니다.&lt;/strong&gt; 이 클래스는 환경 변수와 작업 디렉터리, 출력 리다이렉트를 지정하는 더 편리한 API를 제공합니다. 이 글에서 다룰 출력 파이프 처리도 &lt;code&gt;redirectOutput()&lt;/code&gt;, &lt;code&gt;redirectError()&lt;/code&gt;, &lt;code&gt;redirectErrorStream()&lt;/code&gt; 등으로 구성할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Runtime.exec()&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;ProcessBuilder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실행 시점&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;exec()&lt;/code&gt; 호출 시 실행&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;설정을 구성한 뒤 &lt;code&gt;start()&lt;/code&gt;로 실행&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;명령과 인자&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;문자열 하나 또는 문자열 배열&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;인자를 구분한 가변 인자 또는 리스트&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;환경 변수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;NAME=value&lt;/code&gt; 형식의 배열로 전달&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;environment()&lt;/code&gt;가 반환하는 Map으로 수정&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;작업 디렉터리&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;오버로드의 인자로 전달&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;directory()&lt;/code&gt;로 설정&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;입출력 설정&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;리다이렉트와 표준 오류 병합을 설정하는 API 없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;redirectOutput()&lt;/code&gt;, &lt;code&gt;redirectError()&lt;/code&gt;, &lt;code&gt;redirectErrorStream()&lt;/code&gt;, &lt;code&gt;inheritIO()&lt;/code&gt; 등 제공&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;특히 명령을 문자열 하나로 받는 &lt;code&gt;Runtime.exec(String)&lt;/code&gt; 계열은 JDK 18부터 deprecated입니다. &lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Runtime.html&quot;&gt;Runtime Javadoc&lt;/a&gt;은 이 메서드가 공백만으로 인자를 나누기 때문에 공백이 들어간 파일명 등을 잘못 처리할 수 있다고 설명합니다. 따옴표를 붙여도 셸처럼 인자를 묶어 주지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;// 공백이 있는 파일명을 하나의 인자로 전달하지 못함
Runtime.getRuntime().exec(&quot;cat \&quot;my file.txt\&quot;&quot;);

// 두 방식 모두 파일명을 하나의 인자로 전달
Runtime.getRuntime().exec(new String[]{&quot;cat&quot;, &quot;my file.txt&quot;});
new ProcessBuilder(&quot;cat&quot;, &quot;my file.txt&quot;).start();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;processbuilder_기본_옵션의_한계&quot;&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt; 기본 옵션의 한계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java에서는 다음과 같이 &lt;code&gt;ProcessBuilder&lt;/code&gt; 클래스를 사용하는 몇 줄의 단순한 코드로 외부 프로세스를 실행할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;기본 옵션으로 &lt;code&gt;ProcessBuilder&lt;/code&gt; 호출&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Process process = new ProcessBuilder(&quot;echo&quot;, &quot;hello&quot;).start();
int exitCode = process.waitFor();
System.out.println(&quot;exit=&quot; + exitCode);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;출력이 적고 금방 끝나는 &lt;code&gt;echo hello&lt;/code&gt;에서는 이 코드가 문제없이 동작합니다.
하지만 출력이 많거나 표준 입력을 기다리거나 끝나지 않는 명령을 실행하면 이 코드는 멈출 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;표준_출력과_표준_오류의_파이프_처리&quot;&gt;표준 출력과 표준 오류의 파이프 처리&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;부모 프로세스가 읽지 않는 사이에 자식 프로세스가 표준 출력(stdout)이나 표준 오류(stderr) 파이프의 한정된 용량을 다 채우면, 자식 프로세스는 다음 쓰기에서 멈춥니다.&lt;/strong&gt;
그런 상황을 막으려면 표준 출력과 표준 오류를 계속 소비하는 코드가 필요합니다.
다음 두 방법 중 하나를 써야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;별도의 스레드에서 표준 출력과 표준 오류의 스트림을 읽습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;두 출력을 구분해서 처리하거나 참조해야 한다면 스레드 두 개에서 각각 읽어야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;두 출력을 구분할 필요가 없다면 하나로 합쳐 스레드 하나에서 읽을 수도 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;해당 스트림을 읽을 필요가 없다면 파일, 부모의 출력, &lt;code&gt;/dev/null&lt;/code&gt; 중 하나로 리다이렉트해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 나온 &apos;기본 옵션으로 &lt;code&gt;ProcessBuilder&lt;/code&gt; 호출&apos; 코드는 위의 두 방법 중 어느 것도 쓰지 않았습니다.
이어지는 절에서 이 코드가 멈추는 과정을 예제로 재현하고 해결 방법을 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;읽지_않은_파이프_버퍼가_부르는_교착&quot;&gt;읽지 않은 파이프 버퍼가 부르는 교착&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자식 프로세스의 표준 출력과 표준 오류는 &lt;code&gt;ProcessBuilder&lt;/code&gt;에서도 기본값으로는 각각 별도의 파이프로 부모 프로세스에 전달됩니다. 부모가 파이프를 읽지 않으면 파이프 버퍼가 차고, 자식 프로세스는 쓰기를 마치지 못합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;waitFor()&lt;/code&gt;를 호출하지 않아도 이 문제는 생깁니다.&lt;/strong&gt; 부모는 자식 프로세스 시작 후 이어지는 코드를 실행할 수 있지만, 자식 프로세스는 가득 찬 파이프에 쓰려는 &lt;code&gt;write()&lt;/code&gt; 호출이 블록되어 작업을 마치지 못할 수 있습니다. 여기에 부모가 &lt;code&gt;waitFor()&lt;/code&gt;로 자식 프로세스의 종료만 기다리면 서로를 기다리는 교착이 됩니다. 따라서 &lt;code&gt;waitFor()&lt;/code&gt;를 빼도 해결되지 않고, 출력 파이프를 읽거나 리다이렉트해야 합니다. JDK 25의 &lt;code&gt;Process&lt;/code&gt; Javadoc은 이 멈춤과 교착 가능성을 경고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;Process&lt;/code&gt; 클래스의 Javadoc 중&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the process may cause the process to block, or even deadlock.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;번역&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일부 네이티브 플랫폼은 표준 입력과 표준 출력 스트림에 제한된 크기의 버퍼만 제공하므로, 프로세스의 입력 스트림에 제때 쓰지 않거나 출력 스트림을 제때 읽지 않으면 프로세스가 멈추거나 심하면 교착에 빠질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Javadoc에 언급된 &quot;limited buffer size&quot;가 Linux에서는 얼마인지 &lt;a href=&quot;https://man7.org/linux/man-pages/man7/pipe.7.html&quot;&gt;pipe(7)&lt;/a&gt; 매뉴얼에 나옵니다. 커널 2.6.11부터 기본 파이프 용량은 16페이지, 즉 4KiB 페이지 기준 64KiB입니다. 다만 사용자별 파이프 메모리 한도나 시스템 설정에 따라 더 작아질 수 있고, &lt;code&gt;F_SETPIPE_SZ&lt;/code&gt;로 바꿀 수도 있으므로 64KiB를 고정 한계로 가정하면 안 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 코드는 &lt;code&gt;seq 1 100000&lt;/code&gt; 명령으로 588,895바이트를 출력하게 하고 읽지 않은 채 3초를 기다립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;DeadlockDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Process process = new ProcessBuilder(&quot;seq&quot;, &quot;1&quot;, &quot;100000&quot;).start();
boolean finished = process.waitFor(3, TimeUnit.SECONDS);
System.out.println(&quot;finished within 3s: &quot; + finished + &quot;, alive: &quot; + process.isAlive());
long lines = process.inputReader().lines().count();
int exitCode = process.waitFor();
System.out.println(&quot;read &quot; + lines + &quot; lines, exit=&quot; + exitCode + &quot;, alive: &quot; + process.isAlive());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;DeadlockDemo.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;finished within 3s: false, alive: true
read 100000 lines, exit=0, alive: false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;부모 프로세스가 3초를 기다린 뒤에도 자식 프로세스는 살아 있습니다. 파이프 버퍼가 가득 찬 뒤로는 &lt;code&gt;seq&lt;/code&gt;의 &lt;code&gt;write()&lt;/code&gt; 호출이 블록되어 반환되지 않기 때문입니다. 부모가 파이프를 끝까지 읽자 자식 프로세스는 남은 출력을 마저 쓰고 종료 코드 0으로 끝났습니다(&lt;code&gt;alive: false&lt;/code&gt;). 제한 시간이 없었다면 부모도 &lt;code&gt;waitFor()&lt;/code&gt;에서 계속 기다렸을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예제에 쓴 &lt;code&gt;inputReader()&lt;/code&gt; 메서드는 JDK 17에 추가됐습니다. 그 전에는 &lt;code&gt;getInputStream()&lt;/code&gt;을 &lt;code&gt;InputStreamReader&lt;/code&gt;와 &lt;code&gt;BufferedReader&lt;/code&gt;로 감싸야 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;inputReader()&lt;/code&gt;의 활용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;// JDK 17 이전
BufferedReader reader = new BufferedReader(
        new InputStreamReader(process.getInputStream()));
long lines = reader.lines().count();

// JDK 17 이후
long lines = process.inputReader().lines().count();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 코드는 기본 인코딩을 선택하는 기준이 다릅니다. &lt;code&gt;inputReader()&lt;/code&gt;는 &lt;code&gt;native.encoding&lt;/code&gt; 시스템 속성이 가리키는 인코딩으로 디코딩하고, 인자를 넘기지 않은 &lt;code&gt;InputStreamReader&lt;/code&gt;는 &lt;code&gt;Charset.defaultCharset()&lt;/code&gt;을 씁니다. &lt;a href=&quot;https://openjdk.org/jeps/400&quot;&gt;JDK 18부터 기본 문자 인코딩은 UTF-8&lt;/a&gt;이지만, &lt;code&gt;-Dfile.encoding=COMPAT&lt;/code&gt;을 지정하면 기본 인코딩이 이전처럼 운영체제와 로캘 등에 따라 정해집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;inputReader()&lt;/code&gt;는 자식 프로세스의 출력 인코딩을 자동으로 감지하지 않습니다. 자식이 부모 JVM의 &lt;code&gt;native.encoding&lt;/code&gt;과 같은 인코딩으로 출력한다면 이 기본값이 적합합니다. 자식이 UTF-8을 명시적으로 쓰거나 다른 로캘로 실행된다면 출력 인코딩에 맞춰 &lt;code&gt;inputReader(StandardCharsets.UTF_8)&lt;/code&gt;처럼 직접 지정해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;출력을_읽을_때는_두_스트림을_동시에_처리&quot;&gt;출력을 읽을 때는 두 스트림을 동시에 처리&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;표준 출력과 표준 오류를 따로 받아서 가공해야 한다면 파이프 두 개를 동시에 비워야 합니다. 한 파이프를 다 읽은 뒤에 다른 파이프를 읽는 순차 처리로는 부족합니다. 표준 오류 파이프가 먼저 차면 자식 프로세스는 표준 출력을 더 쓰지 못하고, 부모는 표준 출력의 EOF를 기다리는 교착이 생깁니다. 이 경우에는 &lt;code&gt;waitFor()&lt;/code&gt;에 도달하기도 전에 읽기 작업에서 멈춥니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 코드는 표준 오류에 588,895바이트를 쓴 뒤에 표준 출력에 한 줄을 쓰는 명령을 실행하고, 표준 출력을 끝까지 읽은 다음에 표준 오류를 읽습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SequentialReadDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Process process = new ProcessBuilder(&quot;sh&quot;, &quot;-c&quot;, &quot;seq 1 100000 &amp;gt;&amp;amp;2; echo done&quot;).start();
System.out.println(&quot;reading stdout...&quot;);
long outLines = process.inputReader().lines().count();
long errLines = process.errorReader().lines().count();
System.out.println(&quot;stdout &quot; + outLines + &quot;, stderr &quot; + errLines + &quot;, exit=&quot; + process.waitFor());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 코드를 실행하면 첫 줄을 출력한 뒤로는 아무리 기다려도 다음 줄이 나오지 않고, &lt;code&gt;ps&lt;/code&gt;로 확인하면 자식 &lt;code&gt;seq&lt;/code&gt; 프로세스도 그대로 살아 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SequentialReadDemo.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;reading stdout...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;seq&lt;/code&gt;는 표준 오류 파이프가 차서 멈췄고, 부모는 아직 오지 않은 표준 출력의 EOF를 기다립니다. 표준 오류를 읽는 다음 줄은 실행되지 않으므로 교착이 풀리지 않습니다. 두 스트림에 각각 읽기 스레드를 두면 어느 한쪽을 읽느라 다른 쪽을 방치하는 문제를 피할 수 있습니다. 뒤에 나올 &lt;code&gt;PlainJdkRunner&lt;/code&gt; 예제가 그 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 출력을 구분할 필요가 없다면 &lt;code&gt;redirectErrorStream(true)&lt;/code&gt;로 표준 오류를 표준 출력에 합칠 수 있습니다. 이렇게 하면 읽어야 할 파이프가 하나로 줄어서 스레드 하나로 처리할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;MergedReadDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Process process = new ProcessBuilder(&quot;sh&quot;, &quot;-c&quot;, &quot;seq 1 100000 &amp;gt;&amp;amp;2; echo done&quot;)
        .redirectErrorStream(true)
        .start();
long lines = process.inputReader().lines().count();
System.out.println(&quot;read &quot; + lines + &quot; lines, exit=&quot; + process.waitFor());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 명령이지만 이번에는 끝까지 진행합니다. 100,001줄은 표준 오류로 나온 100,000줄과 표준 출력의 &lt;code&gt;done&lt;/code&gt; 한 줄을 합한 수입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;MergedReadDemo.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;read 100001 lines, exit=0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;java_프로세스에서_읽지_않을_출력은_리다이렉트&quot;&gt;Java 프로세스에서 읽지 않을 출력은 리다이렉트&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자식 프로세스의 출력을 부모 프로세스가 참조하거나 가공할 필요가 없다면 Java 코드가 읽어야 할 파이프를 만들지 않는 편이 가장 단순합니다. 기본값인 &lt;code&gt;Redirect.PIPE&lt;/code&gt; 대신 쓸 수 있는 선택지는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Redirect.INHERIT&lt;/code&gt; : 자식 프로세스가 부모의 표준 출력과 표준 오류에 바로 씁니다. (JDK 7부터 지원)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;inheritIO()&lt;/code&gt; 메서드는 부모 프로세스의 세 스트림(표준 입력, 표준 출력, 표준 오류)을 한 번에 물려줍니다. (JDK 7부터 지원)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Redirect.DISCARD&lt;/code&gt; : 출력을 &lt;code&gt;/dev/null&lt;/code&gt;로 보냅니다. (JDK 9부터 지원)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Redirect.to(File)&lt;/code&gt; : 출력을 파일로 보냅니다. (JDK 7부터 지원)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;자식 프로세스의 출력 Redirect&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;// 부모의 표준 출력과 표준 오류로 바로 내보내기
new ProcessBuilder(&quot;seq&quot;, &quot;1&quot;, &quot;100000&quot;)
        .redirectOutput(Redirect.INHERIT)
        .redirectError(Redirect.INHERIT)
        .start()
        .waitFor();

// 표준 입력까지 세 스트림을 한 번에 물려주기
new ProcessBuilder(&quot;seq&quot;, &quot;1&quot;, &quot;100000&quot;)
        .inheritIO()
        .start()
        .waitFor();

// 출력을 /dev/null로 버리기
new ProcessBuilder(&quot;seq&quot;, &quot;1&quot;, &quot;100000&quot;)
        .redirectOutput(Redirect.DISCARD)
        .redirectError(Redirect.DISCARD)
        .start()
        .waitFor();

// 파일로 보내기
new ProcessBuilder(&quot;seq&quot;, &quot;1&quot;, &quot;100000&quot;)
        .redirectOutput(Redirect.to(new File(&quot;seq-out.log&quot;)))
        .redirectError(Redirect.to(new File(&quot;seq-err.log&quot;)))
        .start()
        .waitFor();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 환경에서는 네 방식 모두 앞의 교착 예제와 같은 588,895바이트를 출력하고 정상 종료했습니다.
부모 JVM이 직접 읽어야 하는 자식 출력 파이프를 만들지 않으므로, 파이프를 읽지 않아서 생기는 교착이 없습니다.
다만 &lt;code&gt;INHERIT&lt;/code&gt;로 물려받은 출력 대상이 파이프라면, 예를 들어 부모 JVM의 표준 출력이 파이프로 다른 프로그램에 연결되어 있다면, 그 프로그램이 읽는 속도에 따라 자식의 쓰기가 막히거나 늦어질 수 있습니다.
출력을 버릴 때는 표준 출력과 표준 오류 모두 &lt;code&gt;DISCARD&lt;/code&gt;를 지정합니다. 한쪽만 지정하면 다른 쪽은 기본값인 &lt;code&gt;PIPE&lt;/code&gt;로 남아서 앞의 교착이 다시 생길 수 있습니다. &lt;code&gt;Redirect.to(File)&lt;/code&gt;은 파일이 이미 있으면 기존 내용을 지우고 덮어쓰므로, 실행할 때마다 뒤에 이어 붙이려면 &lt;code&gt;Redirect.appendTo(File)&lt;/code&gt;을 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;표준_입력_닫기와_eof&quot;&gt;표준 입력 닫기와 EOF&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 Javadoc 인용문이 출력과 함께 경고한 표준 입력(stdin)도 기본값은 파이프입니다. 부모 JVM이 이 파이프의 쓰기 끝을 열어 둔 채로 있으면 자식 프로세스는 EOF를 받지 못합니다. 인자 없이 실행한 &lt;code&gt;cat&lt;/code&gt;처럼 표준 입력을 끝까지 읽는 명령은 더 올 입력이 없는데도 계속 기다리고, 부모는 &lt;code&gt;waitFor()&lt;/code&gt;에서 그 명령이 끝나기를 기다립니다. 출력 파이프를 모두 비워도 이 대기는 풀리지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;입력을 보낼 일이 없으면 &lt;code&gt;start()&lt;/code&gt; 직후에 &lt;code&gt;process.getOutputStream().close()&lt;/code&gt;로 쓰기 끝을 닫거나, &lt;code&gt;redirectInput(new File(&quot;/dev/null&quot;))&lt;/code&gt;처럼 빈 입력을 지정합니다. 입력을 보내야 한다면 다 쓴 뒤에 스트림을 닫아야 합니다. 뒤에서 살펴볼 zt-exec과 Apache Commons Exec은 입력을 지정하지 않으면 자식 프로세스의 표준 입력을 바로 닫습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 예제는 &lt;code&gt;cat&lt;/code&gt;을 네 번 실행하면서 표준 입력을 다루는 방식만 바꿉니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;StdinEofDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Process opened = new ProcessBuilder(&quot;cat&quot;).start();
System.out.println(&quot;stdin 열어 둠: finished within 1s=&quot; + opened.waitFor(1, TimeUnit.SECONDS)
        + &quot;, alive=&quot; + opened.isAlive());
opened.destroy();
opened.waitFor();

Process closed = new ProcessBuilder(&quot;cat&quot;).start();
closed.getOutputStream().close();
System.out.println(&quot;stdin 닫음: finished within 1s=&quot; + closed.waitFor(1, TimeUnit.SECONDS)
        + &quot;, exit=&quot; + closed.exitValue());

Process devNull = new ProcessBuilder(&quot;cat&quot;)
        .redirectInput(new File(&quot;/dev/null&quot;))
        .start();
System.out.println(&quot;/dev/null 입력: finished within 1s=&quot; + devNull.waitFor(1, TimeUnit.SECONDS)
        + &quot;, exit=&quot; + devNull.exitValue());

Process fed = new ProcessBuilder(&quot;cat&quot;).start();
try (Writer writer = fed.outputWriter()) {
    writer.write(&quot;hello\n&quot;);
}
String echoed = fed.inputReader().readLine();
System.out.println(&quot;입력 후 닫음: echoed=&quot; + echoed + &quot;, exit=&quot; + fed.waitFor());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음은 실행 결과입니다. stdin에 쓰는 스트림을 열어 둔 첫 번째만 1초 안에 끝나지 못했고, 나머지 셋은 EOF를 받아 바로 종료했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;StdinEofDemo.java 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;stdin 열어 둠: finished within 1s=false, alive=true
stdin 닫음: finished within 1s=true, exit=0
/dev/null 입력: finished within 1s=true, exit=0
입력 후 닫음: echoed=hello, exit=0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 번째 &lt;code&gt;cat&lt;/code&gt;은 &lt;code&gt;destroy()&lt;/code&gt;로 끝냈습니다. 정리하지 않으면 남은 예제가 실행되는 동안 계속 살아 있습니다.
다만 부모 JVM이 끝나면 파이프의 쓰기 끝도 함께 닫히므로 그 시점에는 &lt;code&gt;cat&lt;/code&gt;도 EOF를 받고 종료합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마지막 예제에 쓴 &lt;code&gt;outputWriter()&lt;/code&gt;는 &lt;code&gt;inputReader()&lt;/code&gt;와 함께 JDK 17에 추가된 메서드로, &lt;code&gt;getOutputStream()&lt;/code&gt;을 &lt;code&gt;OutputStreamWriter&lt;/code&gt;와 &lt;code&gt;BufferedWriter&lt;/code&gt;로 감싸는 코드를 대신합니다.
인자 없이 부르면 &lt;code&gt;native.encoding&lt;/code&gt; 시스템 속성의 문자 집합을 쓰고, &lt;code&gt;outputWriter(Charset)&lt;/code&gt; 메서드로 직접 지정할 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;outputWriter()&lt;/code&gt;의 활용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;// JDK 17 이전
try (BufferedWriter writer = new BufferedWriter(
        new OutputStreamWriter(process.getOutputStream()))) {
    writer.write(&quot;hello\n&quot;);
}

// JDK 17 이후
try (BufferedWriter writer = process.outputWriter()) {
    writer.write(&quot;hello\n&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;시간제한과_종료_정책&quot;&gt;시간제한과 종료 정책&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지 설명한 것처럼 자식 프로세스와 연결되는 파이프 세 개를 처리해도 외부 명령이 네트워크나 잠금을 기다리며 멈출 수 있습니다.
그래서 시간제한과 종료 정책이 별도로 필요합니다.
명령이 멈춰 있는 동안 &lt;code&gt;waitFor()&lt;/code&gt;를 부른 스레드는 외부 명령이 끝날 때까지 블로킹됩니다. JDK 8부터 있는 &lt;code&gt;waitFor(long, TimeUnit)&lt;/code&gt; 메서드는 시간이 지나면 &lt;code&gt;false&lt;/code&gt;를 돌려줄 뿐 프로세스를 종료하지 않습니다.
그때는 &lt;code&gt;destroy()&lt;/code&gt;로 정상 종료를 요청하고 유예 시간 뒤에도 살아 있으면 &lt;code&gt;destroyForcibly()&lt;/code&gt;를 쓰거나, 아래 예제처럼 바로 강제 종료할 수 있습니다.
Linux의 OpenJDK 구현에서 &lt;code&gt;destroy()&lt;/code&gt;는 SIGTERM을, &lt;code&gt;destroyForcibly()&lt;/code&gt;는 SIGKILL을 보냅니다.
프로세스는 SIGTERM을 핸들러로 받아서 임시 파일 삭제나 잠금 해제, 자신이 만든 자식 프로세스 정리 같은 마무리를 한 뒤에 끝날 수 있습니다.
SIGKILL은 핸들러를 통한 마무리 기회 없이 강제 종료하므로 정리하지 못한 파일과 후손 프로세스가 남을 수 있습니다. 그래서 정리할 것이 있는 명령에는 &lt;code&gt;destroy()&lt;/code&gt;로 SIGTERM을 먼저 보내고 유예 시간을 두는 편이 안전합니다.
다만 &lt;code&gt;destroyForcibly()&lt;/code&gt;가 반환된 직후에도 프로세스가 잠시 살아 있을 수 있으므로, 종료 완료가 필요하면 &lt;code&gt;waitFor()&lt;/code&gt;로 확인해야 합니다.
아래 예제는 &lt;code&gt;sleep&lt;/code&gt;처럼 정리할 것이 없는 명령만 실행하므로 바로 강제 종료합니다.
두 출력 스트림을 각각 JDK 21의 가상 스레드에서 읽으면서 종료 대기에 1초 제한을 둡니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;PlainJdkRunner.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static void run(String... command) throws IOException, InterruptedException, TimeoutException {
    Process process = new ProcessBuilder(command).start();
    StringBuilder stdout = new StringBuilder();
    StringBuilder stderr = new StringBuilder();
    Thread outPump = Thread.ofVirtual().start(() -&amp;gt; process.inputReader().lines().forEach(l -&amp;gt; stdout.append(l).append(&apos;\n&apos;)));
    Thread errPump = Thread.ofVirtual().start(() -&amp;gt; process.errorReader().lines().forEach(l -&amp;gt; stderr.append(l).append(&apos;\n&apos;)));
    if (!process.waitFor(1, TimeUnit.SECONDS)) {
        process.destroyForcibly().waitFor();
        throw new TimeoutException(&quot;timed out: &quot; + String.join(&quot; &quot;, command));
    }
    outPump.join();
    errPump.join();
    System.out.println(command[0] + &quot;: exit=&quot; + process.exitValue()
            + &quot;, stdout chars=&quot; + stdout.length() + &quot;, stderr=&quot; + stderr.toString().trim());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음은 &lt;code&gt;echo hello&lt;/code&gt;, &lt;code&gt;seq 1 100000&lt;/code&gt;, &lt;code&gt;ls /no-such-dir&lt;/code&gt;, &lt;code&gt;sleep 60&lt;/code&gt; 네 명령을 이 메서드로 실행한 결과입니다. &lt;code&gt;seq&lt;/code&gt;의 588,895바이트 출력도 멈추지 않고, &lt;code&gt;ls&lt;/code&gt;의 표준 오류는 따로 모이고, 1초를 넘긴 &lt;code&gt;sleep&lt;/code&gt;은 예외로 끝납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;PlainJdkRunner.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;echo: exit=0, stdout chars=6, stderr=
seq: exit=0, stdout chars=588895, stderr=
ls: exit=2, stdout chars=0, stderr=ls: cannot access &apos;/no-such-dir&apos;: No such file or directory
timed out: sleep 60&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 예제는 입력을 요구하지 않고 후손 프로세스를 만들지 않는 명령을 대상으로 합니다.
다른 명령에도 안전하게 쓰려면 다음 항목을 더 챙겨야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;표준 입력을 읽는 명령에는 앞 절의 EOF 처리를 더합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;읽기 스레드에서 난 예외를 호출 스레드에 전달하고, 인터럽트나 시간 초과 때도 프로세스와 스트림을 정리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;위 코드의 1초 제한은 &lt;code&gt;waitFor()&lt;/code&gt;에만 적용됩니다. &lt;code&gt;start()&lt;/code&gt;나 뒤이은 종료 대기와 &lt;code&gt;join()&lt;/code&gt;까지 포함한 전체 시간제한이 필요하면 따로 구현합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;출력이 아주 크면 &lt;code&gt;StringBuilder&lt;/code&gt;에 다 담지 말고 줄 단위나 고정 크기 버퍼로 처리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;출력 인코딩이 부모 JVM의 &lt;code&gt;native.encoding&lt;/code&gt;과 다른 명령에는 &lt;code&gt;inputReader(Charset)&lt;/code&gt;으로 인코딩을 지정합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;후손_프로세스의_정리&quot;&gt;후손 프로세스의 정리&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;destroy()&lt;/code&gt;와 &lt;code&gt;destroyForcibly()&lt;/code&gt;는 직접 만든 자식에게만 시그널을 보냅니다.
실행한 명령이 다시 자식 프로세스를 만들었다면 그 후손은 남을 수 있습니다.
자식이 먼저 끝나면 그 자식이 만든 후손 프로세스는 PID 1이나 가장 가까운 subreaper 프로세스의 자식으로 바뀌어 계속 실행됩니다.
subreaper는 &lt;code&gt;prctl(PR_SET_CHILD_SUBREAPER)&lt;/code&gt;로 지정한 프로세스로, 부모를 잃은 후손을 PID 1 대신 넘겨받습니다. systemd는 대부분의 Linux 배포판에서 부팅 때 PID 1로 가장 먼저 실행되어 다른 서비스를 시작하고 관리하는 프로그램이며, 로그인한 사용자마다 &lt;code&gt;systemd --user&lt;/code&gt;라는 별도 인스턴스도 실행합니다. 데스크톱 세션의 &lt;code&gt;systemd --user&lt;/code&gt;가 subreaper의 예입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;후손_프로세스가_남는_사례&quot;&gt;후손 프로세스가 남는 사례&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;후손 프로세스가 남는 상황은 크게 네 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;셸을 거쳐 실행한 명령&lt;/strong&gt;: &lt;code&gt;sh -c &quot;a | b&quot;&lt;/code&gt;나 &lt;code&gt;run.sh&lt;/code&gt; 같은 스크립트를 실행하다가 시간 초과로 종료하는 경우입니다. 스크립트를 실행하는 비대화형 셸은 자신이 받은 SIGTERM을 자식에게 전달하지 않으므로 셸만 끝납니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;래퍼나 런처를 거쳐 실행되는 프로그램&lt;/strong&gt;: &lt;code&gt;npm run&lt;/code&gt;은 Node.js로 실행되는 npm이 &lt;code&gt;sh -c&lt;/code&gt;로 스크립트를 실행하므로, npm 프로세스를 끝내도 셸이 실행한 명령이 남을 수 있습니다. LibreOffice의 &lt;code&gt;soffice&lt;/code&gt;는 &lt;code&gt;oosplash&lt;/code&gt;를 거쳐 실제 작업을 하는 &lt;code&gt;soffice.bin&lt;/code&gt;을 실행하며, &lt;code&gt;oosplash&lt;/code&gt;를 SIGKILL로 끝내면 &lt;code&gt;soffice.bin&lt;/code&gt;은 종료되지 않고 남습니다. &lt;code&gt;git&lt;/code&gt;도 fetch나 clone 중에 &lt;code&gt;ssh&lt;/code&gt;나 &lt;code&gt;git-remote-https&lt;/code&gt;를 자식으로 실행합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;스스로 데몬이 되는 프로그램&lt;/strong&gt;: Gradle daemon, Kotlin 컴파일 데몬, &lt;code&gt;adb start-server&lt;/code&gt;, &lt;code&gt;ssh&lt;/code&gt;의 ControlPersist 연결은 호출한 쪽이 끝나도 남아 있도록 만들어졌습니다. 이런 프로그램은 &lt;code&gt;setsid()&lt;/code&gt;로 원래 세션과 그룹을 벗어나기도 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;백그라운드 작업을 기다리지 않는 스크립트&lt;/strong&gt;: &lt;code&gt;server &amp;amp;&lt;/code&gt; 뒤에 &lt;code&gt;wait&lt;/code&gt;가 없으면 스크립트가 정상 종료해도 &lt;code&gt;server&lt;/code&gt;는 계속 실행됩니다. 이 후손이 물려받은 출력 파이프를 열어 두면 &lt;code&gt;waitFor()&lt;/code&gt;는 반환되어도 출력을 읽는 쪽은 EOF를 받지 못합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 번째 경우를 재현해 보겠습니다. 아래 코드는 셸로 &lt;code&gt;sleep 60&lt;/code&gt;을 실행하고 0.3초 뒤에 &lt;code&gt;destroy()&lt;/code&gt;를 호출한 다음, 종료 전에 조회해 둔 후손 중 살아 있는 프로세스와 그 부모 PID를 출력합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;DescendantLeakDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static void main(String[] args) throws Exception {
    run(&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 60&quot;);
    run(&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 60; echo done&quot;);
    run(&quot;sh&quot;, &quot;-c&quot;, &quot;sleep 60 | cat&quot;);
    run(&quot;bash&quot;, &quot;-c&quot;, &quot;sleep 60&quot;);
}

static void run(String... command) throws Exception {
    Process process = new ProcessBuilder(command).start();
    Thread.sleep(300);
    List&amp;lt;ProcessHandle&amp;gt; descendants = process.descendants().toList();
    process.destroy();
    process.waitFor(1, TimeUnit.SECONDS);
    Thread.sleep(100);

    List&amp;lt;String&amp;gt; left = descendants.stream()
            .filter(ProcessHandle::isAlive)
            .map(p -&amp;gt; name(p) + &quot;(ppid=&quot; + p.parent().map(ProcessHandle::pid).orElse(-1L) + &quot;)&quot;)
            .toList();
    System.out.println(String.join(&quot; &quot;, command) + &quot; -&amp;gt; shell alive=&quot; + process.isAlive()
            + &quot;, children=&quot; + descendants.size() + &quot;, left=&quot; + left);
    descendants.forEach(ProcessHandle::destroyForcibly);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;DescendantLeakDemo.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;sh -c sleep 60 -&amp;gt; shell alive=false, children=1, left=[sleep(ppid=1693)]
sh -c sleep 60; echo done -&amp;gt; shell alive=false, children=1, left=[sleep(ppid=1693)]
sh -c sleep 60 | cat -&amp;gt; shell alive=false, children=2, left=[sleep(ppid=1693), cat(ppid=1693)]
bash -c sleep 60 -&amp;gt; shell alive=false, children=0, left=[]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sh -c&lt;/code&gt;로 실행한 세 경우 모두 셸은 끝났지만 &lt;code&gt;sleep&lt;/code&gt;과 &lt;code&gt;cat&lt;/code&gt;은 남았습니다. 남은 프로세스의 부모인 PID 1693은 이 환경의 &lt;code&gt;systemd --user&lt;/code&gt;입니다.
명령이 한 줄뿐인 &lt;code&gt;sh -c &quot;sleep 60&quot;&lt;/code&gt;에서도 후손이 남은 점을 주의해야 합니다. Ubuntu 24.04의 &lt;code&gt;/bin/sh&lt;/code&gt;인 dash 0.5.12는 &lt;code&gt;-c&lt;/code&gt;로 받은 마지막 명령도 자식 프로세스로 실행했습니다. 반면 &lt;code&gt;bash -c&lt;/code&gt;는 셸 프로세스를 &lt;code&gt;sleep&lt;/code&gt;으로 바꿔 실행했으므로(&lt;code&gt;children=0&lt;/code&gt;) 남은 후손이 없습니다.
셸의 종류와 버전에 따라 이 동작이 다르므로, 셸을 거친 명령은 후손이 남는다고 가정하고 정리 방법을 준비하는 편이 안전합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;남은 후손이 곧 끝나는 명령이라면 문제는 그 후손이 실행되는 동안만 이어집니다. 그래도 그동안 시간 초과로 실패 처리한 작업이 뒤에서 계속 파일을 쓰거나 외부 API를 호출할 수 있습니다.
더 큰 문제는 스스로 끝나지 않는 후손입니다. 시간 초과가 났다는 것 자체가 명령이 제시간에 끝나지 않았다는 뜻이므로, 남은 후손도 곧 끝난다고 기대하기 어렵습니다. 서버나 데몬처럼 원래 끝나지 않는 후손은 실행할 때마다 쌓여 메모리와 프로세스 수 한도를 차지합니다. 포트나 파일 잠금을 계속 잡고 있으면 다음 실행이 &lt;code&gt;Address already in use&lt;/code&gt; 같은 오류로 실패합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;남은 후손이 물려받은 출력 파이프를 계속 열고 있으면, 진행 중인 읽기가 후손의 파이프 닫기를 기다리면서 &lt;code&gt;join()&lt;/code&gt;도 지연될 수 있습니다. JDK 25의 &lt;code&gt;Process&lt;/code&gt; 구현은 직접 자식이 종료하면 파이프에 남은 출력을 회수하고 스트림을 닫지만, 이미 진행 중인 읽기와 이 종료 처리의 순서에 따라 대기 여부가 달라집니다.
위 예제처럼 JDK 9의 &lt;code&gt;ProcessHandle.descendants()&lt;/code&gt;로 후손을 조회해 하나씩 종료할 수도 있습니다. 하지만 그 결과는 스냅샷이므로, 조회와 종료 사이에 생기거나 부모 종료 뒤 재부모화된 프로세스까지 확실하게 정리하지는 못합니다.
후손까지 한 번에 끝내려면 OS 수준의 프로세스 그룹이나 cgroup으로 관리해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;프로세스_그룹_단위의_종료&quot;&gt;프로세스 그룹 단위의 종료&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;후손이 같은 프로세스 그룹에 남는 명령은 &lt;code&gt;setsid&lt;/code&gt;로 새 세션과 그룹을 만들어 실행하고, 작업이 끝나면 그 그룹 전체에 시그널을 보내 정리할 수 있습니다.
JDK API에는 프로세스 그룹을 만들거나 그룹에 시그널을 보내는 기능이 없으므로, &lt;code&gt;setsid&lt;/code&gt;와 &lt;code&gt;kill&lt;/code&gt; 명령을 함께 실행하는 식으로 구성합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ProcessGroupKillDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static void main(String[] args) throws Exception {
    run(&quot;sleep 60 | cat&quot;);
    run(&quot;(trap &apos;&apos; TERM; sleep 60) &amp;amp; sleep 60&quot;);
    run(&quot;setsid sleep 60 &amp;amp; sleep 60&quot;);
}

static void run(String script) throws Exception {
    Process process = new ProcessBuilder(&quot;setsid&quot;, &quot;sh&quot;, &quot;-c&quot;, script).start();
    List&amp;lt;ProcessHandle&amp;gt; descendants = List.of();
    try {
        if (!process.waitFor(1, TimeUnit.SECONDS)) {
            descendants = process.descendants().toList(); // 결과 확인용 스냅샷
        }
    } finally {
        killGroup(process.pid()); // setsid로 그룹 리더가 된 자식의 PID가 곧 프로세스 그룹 ID
        process.waitFor();
    }

    List&amp;lt;String&amp;gt; left = descendants.stream()
            .filter(ProcessHandle::isAlive)
            .map(p -&amp;gt; p.info().commandLine().orElse(&quot;?&quot;))
            .toList();
    System.out.println(&quot;[&quot; + script + &quot;] pgid=&quot; + process.pid() + &quot;, left=&quot; + left);
    descendants.forEach(ProcessHandle::destroyForcibly);
}

static void killGroup(long pgid) throws Exception {
    signalGroup(&quot;TERM&quot;, pgid);
    if (!awaitGroupExit(pgid, 5)) {
        signalGroup(&quot;KILL&quot;, pgid);
        awaitGroupExit(pgid, 5);
    }
}

static boolean awaitGroupExit(long pgid, long seconds) throws Exception {
    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds);
    while (signalGroup(&quot;0&quot;, pgid)) {
        if (System.nanoTime() &amp;gt; deadline) {
            return false;
        }
        Thread.sleep(100);
    }
    return true;
}

// kill의 종료 코드 0은 그룹에 시그널을 받은 프로세스가 하나 이상 있다는 뜻
static boolean signalGroup(String signal, long pgid) throws Exception {
    Process kill = new ProcessBuilder(&quot;kill&quot;, &quot;-&quot; + signal, &quot;--&quot;, &quot;-&quot; + pgid)
            .redirectErrorStream(true)
            .redirectOutput(Redirect.DISCARD)
            .start();
    return kill.waitFor() == 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;util-linux의 &lt;code&gt;setsid&lt;/code&gt; 명령은 호출한 프로세스가 이미 그룹 리더일 때만 &lt;code&gt;fork()&lt;/code&gt;하고, 그렇지 않으면 자기 프로세스에서 &lt;code&gt;setsid()&lt;/code&gt;를 호출한 뒤 명령을 실행합니다. JVM이 만든 자식은 그룹 리더가 아니므로 PID가 바뀌지 않고, &lt;code&gt;process.pid()&lt;/code&gt;를 새 그룹의 ID로 쓸 수 있습니다.
&lt;code&gt;kill&lt;/code&gt;의 PID 인자에 음수를 넘기면 그 절댓값을 ID로 쓰는 프로세스 그룹 전체에 시그널이 갑니다. 단, &lt;code&gt;-1&lt;/code&gt;은 그룹이 아니라 권한이 있는 모든 프로세스를 뜻합니다. kill은 &lt;code&gt;-9&lt;/code&gt;나 &lt;code&gt;-KILL&lt;/code&gt;처럼 &lt;code&gt;-&lt;/code&gt;로 시작하는 인자를 시그널로 받으므로, &lt;code&gt;--&lt;/code&gt;를 앞에 두어 &lt;code&gt;-295952&lt;/code&gt;를 PID로 읽게 합니다. 시그널 번호 0은 실제로 시그널을 보내지 않고 대상의 존재와 권한만 확인하므로, &lt;code&gt;kill -0&lt;/code&gt;이 성공하면 그룹에 프로세스가 남아 있다는 뜻입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;waitFor()&lt;/code&gt;는 직접 자식인 셸만 기다리므로, 셸이 끝났다고 그룹 전체가 끝났다고 볼 수 없습니다. 그래서 &lt;code&gt;killGroup()&lt;/code&gt;은 SIGTERM을 보낸 뒤 &lt;code&gt;kill -0&lt;/code&gt;으로 그룹에 남은 프로세스가 있는지 확인하면서 최대 5초를 기다리고, 그 뒤에도 남아 있으면 그룹 전체에 SIGKILL을 보냅니다.
정리는 시간 초과 때만이 아니라 &lt;code&gt;finally&lt;/code&gt;에서 항상 합니다. 직접 자식이 정상 종료해도 백그라운드로 띄운 후손은 남을 수 있기 때문입니다. 예를 들어 &lt;code&gt;sleep 60 &amp;amp;&lt;/code&gt;만 실행하는 스크립트는 셸이 바로 끝나서 &lt;code&gt;waitFor()&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt;를 돌려주지만, &lt;code&gt;finally&lt;/code&gt;의 &lt;code&gt;killGroup()&lt;/code&gt;이 남은 &lt;code&gt;sleep&lt;/code&gt;을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ProcessGroupKillDemo.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;[sleep 60 | cat] pgid=295952, left=[]
[(trap &apos;&apos; TERM; sleep 60) &amp;amp; sleep 60] pgid=295959, left=[]
[setsid sleep 60 &amp;amp; sleep 60] pgid=296018, left=[/usr/bin/sleep 60]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 번째 명령의 &lt;code&gt;sleep&lt;/code&gt;과 &lt;code&gt;cat&lt;/code&gt;은 셸과 같은 그룹에 있으므로 SIGTERM으로 함께 끝났습니다.
두 번째 명령의 셸은 SIGTERM으로 바로 끝났지만, SIGTERM을 무시하도록 실행한 &lt;code&gt;sleep&lt;/code&gt;은 5초 유예 뒤 SIGKILL로 끝났습니다. 셸의 종료만 보고 SIGKILL을 생략했다면 이 &lt;code&gt;sleep&lt;/code&gt;은 남았을 것입니다.
세 번째 명령에서 &lt;code&gt;setsid&lt;/code&gt;로 실행한 &lt;code&gt;sleep&lt;/code&gt;은 새 세션과 그룹으로 이동해서 그룹에 보낸 시그널을 받지 않았습니다. 후손이 &lt;code&gt;setsid()&lt;/code&gt;나 &lt;code&gt;setpgid()&lt;/code&gt;로 새 세션이나 다른 그룹으로 이동하면 이 방법으로는 정리하지 못합니다. 앞에서 본 데몬형 프로그램이 이런 경우에 해당합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;cgroup_단위의_종료&quot;&gt;cgroup 단위의 종료&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;cgroup(control group)은 Linux 커널이 프로세스를 계층 구조의 그룹으로 묶어 CPU, 메모리, I/O, 프로세스 수 같은 자원을 제한하고 측정하는 기능입니다. Docker 컨테이너의 메모리 제한도 cgroup으로 동작합니다.
&lt;code&gt;fork()&lt;/code&gt;로 만든 자식은 부모의 cgroup에 속한 채로 시작하므로 손자와 그 아래 후손도 모두 같은 cgroup에 들어갑니다.
&lt;code&gt;setsid()&lt;/code&gt;는 세션과 프로세스 그룹만 바꾸고 cgroup은 바꾸지 않습니다. cgroup v2에서 프로세스를 다른 cgroup으로 옮기려면 원래 cgroup과 옮길 cgroup의 공통 조상에 있는 &lt;code&gt;cgroup.procs&lt;/code&gt; 파일에 쓸 권한이 필요합니다. 그래서 권한이 없는 후손은 cgroup을 벗어나지 못합니다.
현재 프로세스가 속한 cgroup은 &lt;code&gt;/proc/self/cgroup&lt;/code&gt;에서, 전체 트리는 &lt;code&gt;systemd-cgls&lt;/code&gt; 명령으로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;systemd는 서비스마다 cgroup을 따로 만들고, 서비스를 멈출 때 그 cgroup에 남은 프로세스를 모두 종료합니다(&lt;code&gt;KillMode=control-group&lt;/code&gt;이 기본값).
&lt;code&gt;systemd-run --scope&lt;/code&gt;를 쓰면 한 번 실행할 명령에도 같은 방식을 적용할 수 있습니다. 아래 코드는 앞에서 프로세스 그룹을 벗어난 명령을 임시 scope 단위로 실행하고, 작업이 끝나면 그 단위를 멈춥니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SystemdScopeDemo.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;String unit = &quot;job-&quot; + UUID.randomUUID() + &quot;.scope&quot;;
Process process = new ProcessBuilder(
        &quot;systemd-run&quot;, &quot;--user&quot;, &quot;--scope&quot;, &quot;--quiet&quot;, &quot;--unit=&quot; + unit,
        &quot;sh&quot;, &quot;-c&quot;, &quot;setsid sleep 60 &amp;amp; sleep 60&quot;)
        .redirectError(Redirect.INHERIT)
        .start();
List&amp;lt;ProcessHandle&amp;gt; descendants = List.of();
try {
    if (process.waitFor(1, TimeUnit.SECONDS)) {
        System.out.println(&quot;exit=&quot; + process.exitValue()); // systemd-run의 실패도 여기서 드러남
    } else {
        System.out.println(&quot;cgroup: &quot; + Files.readString(Path.of(&quot;/proc/&quot; + process.pid() + &quot;/cgroup&quot;)).trim());
        descendants = process.descendants().toList(); // 결과 확인용 스냅샷
    }
} finally {
    int stopExit = new ProcessBuilder(&quot;systemctl&quot;, &quot;--user&quot;, &quot;stop&quot;, unit)
            .redirectError(Redirect.INHERIT)
            .start()
            .waitFor();
    System.out.println(&quot;systemctl stop exit=&quot; + stopExit);
    if (!process.waitFor(10, TimeUnit.SECONDS)) {
        process.destroyForcibly().waitFor();
    }
}

List&amp;lt;String&amp;gt; left = descendants.stream()
        .filter(ProcessHandle::isAlive)
        .map(p -&amp;gt; p.info().commandLine().orElse(&quot;?&quot;))
        .toList();
System.out.println(&quot;descendants=&quot; + descendants.size() + &quot;, left=&quot; + left);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SystemdScopeDemo.java의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;cgroup: 0::/user.slice/user-1000.slice/user@1000.service/app.slice/job-0fbacb17-d0c0-47de-a65d-db30d90ecfb1.scope
systemctl stop exit=0
descendants=2, left=[]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--scope&lt;/code&gt;로 실행하면 &lt;code&gt;systemd-run&lt;/code&gt;은 scope 단위의 시작을 기다린 뒤 자기 프로세스를 명령으로 바꿔 실행하므로 PID가 바뀌지 않습니다. 명령은 여전히 JVM의 직접 자식이므로 앞의 예제들처럼 &lt;code&gt;Process&lt;/code&gt;로 출력을 읽고 종료를 기다릴 수 있습니다.
실행 결과의 cgroup 경로가 &lt;code&gt;job-&amp;#8230;&amp;#8203;scope&lt;/code&gt;로 끝나는 것으로 명령이 전용 cgroup에서 실행됐음을 확인할 수 있습니다. &lt;code&gt;systemctl stop&lt;/code&gt;을 호출하자 &lt;code&gt;setsid&lt;/code&gt;로 그룹을 벗어난 &lt;code&gt;sleep&lt;/code&gt;까지 두 후손이 모두 끝났습니다.
&lt;code&gt;systemctl stop&lt;/code&gt;은 SIGTERM을 먼저 보내고, 유예 시간(&lt;code&gt;TimeoutStopSec&lt;/code&gt;) 안에 끝나지 않은 프로세스에 SIGKILL을 보냅니다. 유예 시간은 &lt;code&gt;systemd-run&lt;/code&gt;에 &lt;code&gt;-p TimeoutStopSec=5s&lt;/code&gt;처럼 지정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;scope는 특정 프로세스가 아니라 그 안에 프로세스가 하나라도 남아 있는 동안 유지됩니다. 셸이 먼저 끝나도 백그라운드로 띄운 후손이 있으면 scope는 활성 상태로 남으므로, 앞의 프로세스 그룹 예제처럼 &lt;code&gt;finally&lt;/code&gt;에서 항상 단위를 멈춥니다.
&lt;code&gt;systemd-run&lt;/code&gt;이 사용자 버스에 접속하지 못하는 등의 이유로 실패하면 오류를 표준 오류에 출력하고 0이 아닌 종료 코드로 끝나므로, 예제는 표준 오류를 부모로 넘기고 종료 코드를 출력합니다. &lt;code&gt;systemctl stop&lt;/code&gt;도 종료 코드를 확인하고, 정리가 실패했을 때 무한정 기다리지 않도록 &lt;code&gt;waitFor()&lt;/code&gt;에 제한 시간을 둡니다. 명령이 먼저 끝나 scope가 이미 사라졌다면 &lt;code&gt;systemctl stop&lt;/code&gt;은 단위가 없다는 뜻의 종료 코드 5를 돌려주므로, 실제 코드에서는 이 경우를 정상으로 처리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 방법에는 전제 조건이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--user&lt;/code&gt;를 쓰려면 사용자의 systemd 인스턴스가 실행 중이어야 합니다. 로그인 세션이 없는 서버의 서비스 계정이라면 &lt;code&gt;loginctl enable-linger&lt;/code&gt;로 사용자 인스턴스를 유지하거나, 권한이 있는 system 인스턴스를 써야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;대부분의 컨테이너 안에는 systemd가 없으므로 이 방법을 쓸 수 없습니다. 이때는 작업마다 별도 컨테이너를 실행해 컨테이너 단위로 정리하는 방법을 고려할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;systemd 없이 cgroup v2를 직접 다룰 권한을 위임받았다면, 작업용 cgroup을 만들고 그 안에서 명령을 시작한 뒤 &lt;code&gt;cgroup.kill&lt;/code&gt; 파일에 &lt;code&gt;1&lt;/code&gt;을 써서 하위 cgroup까지 모든 프로세스에 SIGKILL을 보낼 수 있습니다. 이 파일은 Linux 5.14부터 지원합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어느 방법이든 대상 작업을 처음부터 해당 cgroup 안에서 시작하고, 그 밖으로 이동할 권한과 종료 정책을 함께 관리해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하면 실행하는 명령을 직접 통제하고 그 명령이 데몬이 되지 않는다면 프로세스 그룹 방식으로 충분합니다. 임의의 스크립트나 외부에서 받은 명령을 실행한다면 cgroup 단위로 정리하는 편이 확실합니다.
다음 절의 라이브러리들도 출력 처리와 시간제한 코드를 줄여 주지만, 이런 조건까지 모두 해결해 주지는 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;zt_exec과_apache_commons_exec의_활용&quot;&gt;zt-exec과 Apache Commons Exec의 활용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞 절에서 본 출력 파이프, 표준 입력의 EOF, 시간제한, 종료 정책을 모두 챙긴 코드를 직접 작성하기는 쉽지 않습니다.
원리를 잘 이해한다고 해도 이 코드를 프로젝트마다 다시 구현하는 일은 번거롭습니다.
이런 처리가 반영된 라이브러리를 사용하는 편이 실무에서는 실용적입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 분야의 대표적인 라이브러리가 zt-exec과 Apache Commons Exec입니다.
두 라이브러리 모두 별도의 스레드로 출력 파이프를 비우고, 입력을 지정하지 않으면 &lt;code&gt;PumpStreamHandler&lt;/code&gt;라는 클래스가 자식 프로세스의 표준 입력을 닫아서 자식이 EOF를 받게 합니다.
zt-exec의 &lt;code&gt;PumpStreamHandler&lt;/code&gt; 클래스는 소스 파일 상단에 &quot;This file originates from the Apache Commons Exec package&quot;라고 적혀 있을 만큼 Commons Exec의 코드를 가져온 것입니다. 이 글에서 비교할 주요 차이는 API의 형태와 기본값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;zt_exec&quot;&gt;zt-exec&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zeroturnaround/zt-exec&quot;&gt;zt-exec&lt;/a&gt;은 JRebel을 만든 ZeroTurnaround가 사내 여러 프로젝트에 흩어져 있던 프로세스 실행 코드를 하나로 합쳐서 2013년에 공개한 라이브러리입니다.
정식 이름은 &lt;a href=&quot;https://github.com/zeroturnaround/zt-exec/blob/v1.13.0/README.md&quot;&gt;README&lt;/a&gt;의 제목대로 ZT Process Executor이지만, GitHub 저장소와 Maven artifactId가 zt-exec이어서 보통 zt-exec으로 부릅니다. 이 글에서도 zt-exec으로 씁니다.
README는 &lt;code&gt;ProcessExecutor&lt;/code&gt; 클래스 하나로 &lt;code&gt;ProcessBuilder&lt;/code&gt;와 Commons Exec의 기능을 모두 제공하는 것이 목표라고 설명합니다.
의존성은 slf4j-api 하나이고 최소 Java 버전은 8입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.zeroturnaround&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;zt-exec&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.13.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;zt-exec의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;기본값이 안전합니다.&lt;/strong&gt; 아무 설정 없이 &lt;code&gt;execute()&lt;/code&gt;만 부르면 표준 오류를 표준 출력에 합치고 그 출력을 버립니다. 파이프를 읽지 않아서 생기는 교착이 기본값에서는 일어나지 않습니다. 출력이 필요하면 &lt;code&gt;readOutput(true)&lt;/code&gt;를 켜고 결과의 &lt;code&gt;outputUTF8()&lt;/code&gt;로 문자열을 받거나, &lt;code&gt;redirectOutput()&lt;/code&gt;에 줄 단위로 문자열을 처리하는 함수를 람다 표현식으로 넘깁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;시간제한이 예외 타입으로 구분됩니다.&lt;/strong&gt; &lt;code&gt;execute()&lt;/code&gt;의 대기가 &lt;code&gt;timeout()&lt;/code&gt;으로 지정한 시간을 넘기면 호출자는 &lt;code&gt;TimeoutException&lt;/code&gt;을 받고, 작업 스레드는 stopper로 종료를 시도합니다. 종료 코드가 잘못된 경우와 시간 초과를 예외 타입만으로 구분할 수 있습니다. 다만 예외를 잡은 시점에 stopper의 종료 처리가 완료됐다고 보장되지는 않습니다. 기본 stopper는 &lt;code&gt;destroy()&lt;/code&gt;만 호출하므로 Linux에서 SIGTERM을 무시하는 프로세스까지 강제 종료하지는 않습니다. 종료 방식은 &lt;code&gt;stopper()&lt;/code&gt;로 바꿀 수 있습니다. &lt;code&gt;ProcessStopper&lt;/code&gt; 인터페이스를 구현해서 &lt;code&gt;destroy()&lt;/code&gt; 뒤에 유예 시간을 두고 &lt;code&gt;destroyForcibly()&lt;/code&gt;를 부르는 정책을 넣으면 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;종료 코드 검사를 선언합니다.&lt;/strong&gt; 기본값은 모든 종료 코드를 허용하고, &lt;code&gt;exitValueNormal()&lt;/code&gt;이나 &lt;code&gt;exitValues(0, 1)&lt;/code&gt;로 허용 범위를 지정하면 그 밖의 값에서 &lt;code&gt;InvalidExitValueException&lt;/code&gt;이 발생합니다. 예외 객체의 &lt;code&gt;getResult()&lt;/code&gt;로 그때까지의 출력도 볼 수 있어서 오류 메시지를 로그에 남기기 좋습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;부수 기능을 간단히 설정합니다.&lt;/strong&gt; &lt;code&gt;start().getFuture()&lt;/code&gt;로 비동기로 실행할 수 있습니다. 다만 &lt;code&gt;start()&lt;/code&gt;는 &lt;code&gt;timeout()&lt;/code&gt; 설정을 무시하므로, &lt;code&gt;Future.get(timeout, unit)&lt;/code&gt; 등으로 대기를 제한하고 시간 초과 때 취소·종료 처리를 따로 해야 합니다. &lt;code&gt;get()&lt;/code&gt;의 시간 초과만으로 프로세스가 종료되지는 않습니다. &lt;code&gt;destroyOnExit()&lt;/code&gt;로는 JVM의 shutdown hook에서 자식 프로세스에 종료를 요청합니다. &lt;code&gt;redirectOutputAsInfo()&lt;/code&gt;로 SLF4J 로거에 출력을 전달할 수도 있지만, 이 메서드는 deprecated이며 &lt;code&gt;redirectOutput(Slf4jStream.of(logger).asInfo())&lt;/code&gt;가 권장 API입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞 절의 네 명령을 zt-exec으로 실행한 예제입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ZtExecRunner.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static void captureOutput() throws IOException, InterruptedException, TimeoutException {
    ProcessResult result = new ProcessExecutor()
            .command(&quot;echo&quot;, &quot;hello&quot;)
            .readOutput(true)
            .exitValueNormal()
            .execute();
    System.out.println(&quot;output=&quot; + result.outputUTF8().trim() + &quot;, exit=&quot; + result.getExitValue());
}

static void largeOutput() throws IOException, InterruptedException, TimeoutException {
    AtomicLong lines = new AtomicLong();
    ProcessResult result = new ProcessExecutor()
            .command(&quot;seq&quot;, &quot;1&quot;, &quot;100000&quot;)
            .redirectOutput(line -&amp;gt; lines.incrementAndGet())
            .timeout(3, TimeUnit.SECONDS)
            .execute();
    System.out.println(&quot;seq lines=&quot; + lines.get() + &quot;, exit=&quot; + result.getExitValue());
}

static void timeout() throws IOException, InterruptedException {
    try {
        new ProcessExecutor()
                .command(&quot;sleep&quot;, &quot;60&quot;)
                .timeout(1, TimeUnit.SECONDS)
                .execute();
    } catch (TimeoutException e) {
        System.out.println(&quot;timeout: &quot; + e.getMessage());
    }
}

static void exitValue() throws IOException, InterruptedException, TimeoutException {
    try {
        new ProcessExecutor()
                .command(&quot;ls&quot;, &quot;/no-such-dir&quot;)
                .readOutput(true)
                .exitValueNormal()
                .execute();
    } catch (InvalidExitValueException e) {
        System.out.println(&quot;exit=&quot; + e.getExitValue() + &quot;, output=&quot; + e.getResult().outputUTF8().trim());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음은 실행 결과입니다. &lt;code&gt;TimeoutException&lt;/code&gt;의 메시지에 실행한 명령과 제한 시간이 들어 있고, &lt;code&gt;ls&lt;/code&gt;의 오류 메시지는 표준 오류가 표준 출력에 합쳐지는 기본값 덕분에 &lt;code&gt;outputUTF8()&lt;/code&gt;로 읽힙니다. 시간 초과 뒤에 &lt;code&gt;sleep&lt;/code&gt; 프로세스가 남아 있지 않은 것도 확인했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ZtExecRunner.java 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;output=hello, exit=0
seq lines=100000, exit=0
timeout: Timed out waiting for Process[pid=294823, exitValue=&quot;not exited&quot;] to finish, timeout: 1 second, executed command [sleep, 60]
exit=2, output=ls: cannot access &apos;/no-such-dir&apos;: No such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주의할 점도 있습니다. &lt;code&gt;readOutput(true)&lt;/code&gt;는 출력 파이프를 읽는 스레드가 읽은 바이트를 전부 &lt;code&gt;ByteArrayOutputStream&lt;/code&gt;에 저장합니다. 결과를 만들 때 &lt;code&gt;toByteArray()&lt;/code&gt;로 한 번 더 복사하고, &lt;code&gt;outputUTF8()&lt;/code&gt;을 부르면 문자열도 만듭니다. &lt;code&gt;ByteArrayOutputStream&lt;/code&gt;은 버퍼가 차면 크기를 두 배로 늘리므로 내부 버퍼만으로도 출력 크기보다 커질 수 있고, 한글처럼 Latin-1 밖의 문자가 섞인 문자열은 글자당 2바이트를 씁니다. 그래서 최대 메모리 사용량이 출력 크기의 두 배를 넘을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전체 출력을 보관할 필요가 없다면 위 예제의 &lt;code&gt;largeOutput()&lt;/code&gt;처럼 &lt;code&gt;readOutput&lt;/code&gt;을 켜지 않고 &lt;code&gt;redirectOutput()&lt;/code&gt;에 줄 단위 처리 함수를 넘길 수 있습니다. 이 방식의 버퍼 크기는 총 출력량보다는 가장 긴 줄의 크기에 좌우됩니다. 줄바꿈 없는 거대한 출력에는 여전히 메모리가 많이 들고, 처리 함수가 느리면 자식 프로세스의 출력도 밀립니다. 그런 출력은 파일이나 고정 크기 버퍼로 처리하는 &lt;code&gt;OutputStream&lt;/code&gt;으로 보내는 편이 낫습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 예제는 SLF4J API 2.0.17을 사용했으므로 구현체가 없을 때 초기화 과정에서 &quot;No SLF4J providers were found&quot;를 포함한 경고가 세 줄 나왔습니다. zt-exec 1.13.0의 POM이 선언한 의존성은 SLF4J API 1.7.32이며, 이 버전의 경고 문구는 다릅니다. 로그가 필요 없다면 사용 중인 SLF4J API에 맞는 &lt;code&gt;slf4j-nop&lt;/code&gt;을 추가해 경고를 없앨 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;apache_commons_exec&quot;&gt;Apache Commons Exec&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://commons.apache.org/proper/commons-exec/&quot;&gt;Apache Commons Exec&lt;/a&gt;은 명령 실행, 출력 스트림 처리, 시간제한을 각각 &lt;code&gt;DefaultExecutor&lt;/code&gt;, &lt;code&gt;PumpStreamHandler&lt;/code&gt;, &lt;code&gt;ExecuteWatchdog&lt;/code&gt;로 구성하는 라이브러리입니다. 이 글에서 사용하는 1.6.0은 Java 8 이상에서 실행되며 외부 의존성이 없습니다. &lt;code&gt;DefaultExecutor&lt;/code&gt;와 &lt;code&gt;ExecuteWatchdog&lt;/code&gt;는 builder API로 만들고, 시간제한은 &lt;code&gt;Duration&lt;/code&gt;으로 지정합니다. 기존 생성자는 deprecated로 표시되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.commons&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;commons-exec&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.6.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 네 명령을 1.6.0으로 실행한 예제입니다. 스트림 처리를 맡는 &lt;code&gt;PumpStreamHandler&lt;/code&gt; 클래스는 기본으로 &lt;code&gt;System.out&lt;/code&gt;과 &lt;code&gt;System.err&lt;/code&gt;에 출력하므로, 출력을 모으려면 아래처럼 &lt;code&gt;OutputStream&lt;/code&gt;을 직접 넘깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;CommonsExecRunner.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static void run(String... command) throws IOException {
    CommandLine cmdLine = new CommandLine(command[0]);
    cmdLine.addArguments(Arrays.copyOfRange(command, 1, command.length), false);
    ByteArrayOutputStream stdout = new ByteArrayOutputStream();
    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
    ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofSeconds(1)).get();
    DefaultExecutor executor = DefaultExecutor.builder().get();
    executor.setStreamHandler(new PumpStreamHandler(stdout, stderr));
    executor.setWatchdog(watchdog);
    try {
        int exitValue = executor.execute(cmdLine);
        System.out.println(command[0] + &quot;: exit=&quot; + exitValue + &quot;, stdout bytes=&quot; + stdout.size());
    } catch (ExecuteException e) {
        System.out.println(command[0] + &quot;: exit=&quot; + e.getExitValue()
                + &quot;, killed by watchdog=&quot; + watchdog.killedProcess()
                + &quot;, stderr=&quot; + stderr.toString().trim());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;CommonsExecRunner.java 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;echo: exit=0, stdout bytes=6
seq: exit=0, stdout bytes=588895
sleep: exit=143, killed by watchdog=true, stderr=
ls: exit=2, killed by watchdog=false, stderr=ls: cannot access &apos;/no-such-dir&apos;: No such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;DefaultExecutor&lt;/code&gt;는 Linux에서 기본적으로 0이 아닌 종료 코드에 &lt;code&gt;ExecuteException&lt;/code&gt;을 던집니다.
에러로 처리하지 않을 종료 코드는 &lt;code&gt;setExitValue()&lt;/code&gt;나 &lt;code&gt;setExitValues()&lt;/code&gt;로 바꿀 수 있고, &lt;code&gt;setExitValues(null)&lt;/code&gt;로 검사를 끌 수도 있습니다.
위 결과의 &lt;code&gt;sleep&lt;/code&gt;은 watchdog이 보낸 SIGTERM으로 끝나서 종료 코드가 143입니다.
이 예제에서는 예외를 잡은 뒤 &lt;code&gt;watchdog.killedProcess()&lt;/code&gt;로 watchdog의 개입 여부를 구분합니다.
다만 이 메서드는 &lt;code&gt;destroy()&lt;/code&gt; 호출 여부를 나타내며 실제 종료를 보장하지 않습니다.
1.6.0의 &lt;code&gt;ExecuteWatchdog&lt;/code&gt;는 시간 초과 때 &lt;code&gt;destroy()&lt;/code&gt;만 호출하고 강제 종료로 넘어가는 API가 없습니다.
강제 종료가 필요하면 시간 초과 뒤 남은 프로세스를 &lt;code&gt;ProcessHandle&lt;/code&gt;로 찾아 &lt;code&gt;destroyForcibly()&lt;/code&gt;를 부르는 코드를 따로 두어야 합니다.
명령이 SIGTERM을 무시하면 &lt;code&gt;execute()&lt;/code&gt;가 계속 기다릴 수 있습니다. 반대로 SIGTERM을 받아 종료 처리를 한 명령이 허용된 코드로 끝나면 시간 초과 뒤에도 예외 없이 반환될 수 있습니다. 그래서 정상 반환 경로에서도 &lt;code&gt;killedProcess()&lt;/code&gt;를 확인해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;유지보수_현황과_선택&quot;&gt;유지보수 현황과 선택&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음은 Maven Central의 버전 목록과 각 프로젝트의 변경 기록으로 2026-09-06에 확인한 두 라이브러리의 현황입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;zt-exec&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Apache Commons Exec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;최신 버전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.13.0 (2026-07-10)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.6.0 (2025-11-25)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;그 이전 릴리스&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.12 (2020-09-02), 1.11 (2019-07-05)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.5.0 (2025-05-16), 1.4.0 (2024-01-01), 1.3 (2014-11-02)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;최근 5년간 릴리스 수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1회&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;3회&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;최소 Java 버전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;8&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;8&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;의존성&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;slf4j-api&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본 출력 처리&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;버림 (표준 오류는 표준 출력에 합침)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;System.out&lt;/code&gt;, &lt;code&gt;System.err&lt;/code&gt;로 전달&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;시간 초과 알림&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;TimeoutException&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;watchdog.killedProcess()&lt;/code&gt;로 확인 (실패 종료 코드이면 &lt;code&gt;ExecuteException&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0이 아닌 종료 코드&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본 허용, &lt;code&gt;exitValues()&lt;/code&gt;로 지정 시 &lt;code&gt;InvalidExitValueException&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본 &lt;code&gt;ExecuteException&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;시간 초과 시 종료 방식&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;destroy()&lt;/code&gt;, &lt;code&gt;stopper()&lt;/code&gt;로 교체 가능&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;destroy()&lt;/code&gt; 고정&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘 중에서 저는 zt-exec을 더 추천합니다.
API 설계에서 세 가지 장점이 있기 때문입니다.
시간 초과와 종료 코드 오류를 예외 타입으로 구분하고, 출력을 문자열로 받는 데 메서드 호출 하나면 되고, 기본 설정에서 출력 파이프를 읽지 않아 생기는 교착을 피할 수 있습니다.
두 라이브러리 모두 기본 종료 요청은 &lt;code&gt;destroy()&lt;/code&gt;입니다. zt-exec은 &lt;code&gt;stopper()&lt;/code&gt;로 강제 종료 정책을 넣을 수 있지만 Commons Exec은 별도 코드가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;linux에서의_프로세스_생성과_운영&quot;&gt;Linux에서의 프로세스 생성과 운영&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;현재 Linux에서 JDK 25를 쓰는 Java 개발자는 &lt;code&gt;jdk.lang.Process.launchMechanism&lt;/code&gt; 옵션을 별도로 지정하지 않고 기본값인 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;을 유지하면 됩니다.&lt;/strong&gt; JDK 13부터 이 방식이 기본입니다.
이 장에서는 그 이유를 확인하기 위해 Linux가 제공하는 세 가지 프로세스 생성 방식과 JDK 기본값의 변천을 먼저 정리하고, &lt;code&gt;POSIX_SPAWN&lt;/code&gt;의 동작, 대안인 &lt;code&gt;FORK&lt;/code&gt;의 메모리 비용과 실행 지연, 마지막으로 JVM을 업그레이드할 때 정리해야 할 &lt;code&gt;VFORK&lt;/code&gt; 설정을 차례로 살펴봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;프로세스_생성_방식_세_가지와_jdk_기본값의_변천&quot;&gt;프로세스 생성 방식 세 가지와 JDK 기본값의 변천&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;애플리케이션의 외부 프로세스 실행 코드는 JVM을 거쳐 커널의 시스템 콜이나 C 라이브러리의 함수 호출로 이어집니다.
어떤 함수 호출로 자식 프로세스를 만드는지가 중요한 이유는 이 선택이 과거에 실제 장애로 이어졌기 때문입니다. 힙을 크게 잡은 JVM이 외부 명령 하나를 실행하려다 메모리 할당에 실패하는 문제가 대표적입니다. 2015년 NAVER D2의 &lt;a href=&quot;https://d2.naver.com/helloworld/1113548&quot;&gt;Java에서 외부 프로세스를 실행할 때&lt;/a&gt;는 힙을 크게 설정한 Tomcat 서버에서 외부 프로세스 실행이 &lt;code&gt;Cannot allocate memory&lt;/code&gt; 예외로 실패하는 사례를 소개합니다. OpenJDK 이슈에도 그 이력이 남아 있습니다. JDK 7에 반영된 &lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-6868160&quot;&gt;JDK-6868160&lt;/a&gt;의 제목은 &quot;(process) Use vfork, not fork, on Linux to avoid swap exhaustion&quot;입니다. 자식 프로세스를 만드는 호출을 바꾼 것이 스왑 고갈을 피하려는 조치였다는 뜻입니다. 지금 기본값을 그대로 두면 되는 것도 이런 문제들이 차례로 해결된 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linux에서 다른 프로그램을 실행하려면 자식 프로세스를 만드는 호출과 그 자식을 다른 프로그램으로 바꾸는 시스템 콜인 &lt;code&gt;execve()&lt;/code&gt;를 짝지어야 합니다. 선택의 여지가 있는 쪽은 앞의 절반, 즉 자식을 만드는 방법입니다. 후보는 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt; : 부모의 주소 공간을 복제한 자식을 만드는 시스템 콜입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vfork()&lt;/code&gt; : 주소 공간을 복제하지 않고 부모와 공유하는 자식을 만드는 시스템 콜입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;posix_spawn()&lt;/code&gt; : 자식 생성과 &lt;code&gt;execve()&lt;/code&gt;를 한 묶음으로 제공하는 C 라이브러리 함수입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linux 커널은 &lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;vfork()&lt;/code&gt;, &lt;code&gt;clone()&lt;/code&gt; 시스템 콜을 모두 같은 함수인 &lt;code&gt;kernel_clone()&lt;/code&gt;으로 처리하고, 부모와 무엇을 공유할지는 플래그로 정합니다. 이 글의 환경인 x86-64의 glibc 2.39에서 &lt;code&gt;fork()&lt;/code&gt; 함수는 &lt;code&gt;clone()&lt;/code&gt; 시스템 콜을 호출하고, &lt;code&gt;vfork()&lt;/code&gt; 함수는 별도의 &lt;code&gt;vfork()&lt;/code&gt; 시스템 콜을 그대로 호출합니다. 같은 glibc 2.39라도 &lt;a href=&quot;https://github.com/bminor/glibc/blob/glibc-2.39/sysdeps/unix/sysv/linux/aarch64/vfork.S&quot;&gt;AArch64의 vfork 구현&lt;/a&gt;은 &lt;code&gt;clone()&lt;/code&gt; 시스템 콜을 사용하므로 아키텍처에 따라 호출 경로가 다릅니다. &lt;a href=&quot;https://man7.org/linux/man-pages/man2/vfork.2.html&quot;&gt;vfork(2)&lt;/a&gt; 매뉴얼은 &lt;code&gt;vfork()&lt;/code&gt;를 &lt;code&gt;CLONE_VM | CLONE_VFORK | SIGCHLD&lt;/code&gt; 플래그를 준 &lt;code&gt;clone()&lt;/code&gt;과 같다고 설명합니다. 뒤에 나올 strace 결과에 이 플래그가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;posix_spawn()&lt;/code&gt;만 계층이 다릅니다. POSIX가 명세하고 C 라이브러리가 구현한 함수여서 대응하는 시스템 콜이 없고, 내부에서는 앞의 두 방식 중 하나나 &lt;code&gt;vfork()&lt;/code&gt;와 같은 플래그를 준 &lt;code&gt;clone()&lt;/code&gt;을 골라 씁니다.
세 방식 모두 JDK가 직접 구현하지 않고 glibc의 함수를 호출합니다.
JVM의 프로세스 생성 코드가 들어 있는 &lt;code&gt;libjava.so&lt;/code&gt;의 동적 심볼에서 이를 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;$ nm -D $JAVA_HOME/lib/libjava.so | grep -E &apos;posix_spawn|fork&apos;
                 U fork@GLIBC_2.2.5
00000000000125c0 T Java_java_lang_ProcessImpl_forkAndExec
                 U posix_spawn@GLIBC_2.15
                 U vfork@GLIBC_2.2.5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;T&lt;/code&gt;는 이 파일이 정의한 심볼, &lt;code&gt;U&lt;/code&gt;는 정의하지 않고 실행 시점에 다른 라이브러리에서 찾는 심볼입니다.
OpenJDK가 만든 것은 네이티브 메서드인 &lt;code&gt;forkAndExec&lt;/code&gt; 이고, 세 실행 방식에 해당하는 함수는 모두 &lt;code&gt;@GLIBC_&lt;/code&gt; 버전 태그가 붙은 glibc 심볼로 연결되어 있습니다.
그래서 같은 &lt;code&gt;POSIX_SPAWN&lt;/code&gt; 설정이라도 libc와 그 버전에 따라 실제 동작이 달라집니다.
glibc가 아닌 musl에 링크된 JDK라면 같은 자리에 musl의 구현이 들어갑니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세 방식의 장단점은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;방식&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;장점&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;fork()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;자식의 주소 공간이 분리되어 부모 메모리를 훼손하지 않고 파일 디스크립터 정리나 작업 디렉터리 변경 같은 준비 작업을 할 수 있음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;쓰기 시 복사(copy-on-write)로 페이지 복사는 미루지만 페이지 테이블은 복사하므로 힙이 클수록 생성 비용이 커지고, 커널의 overcommit 판정 대상이 됨&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;vfork()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;페이지 테이블을 복사하지 않아 생성 비용이 부모의 힙 크기와 거의 무관함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;자식이 &lt;code&gt;execve()&lt;/code&gt;나 &lt;code&gt;_exit()&lt;/code&gt; 외의 일을 하면 동작이 정의되지 않고, 그동안 부모의 호출 스레드도 멈춤&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;posix_spawn()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;준비 작업을 라이브러리가 정한 방식으로 처리하고, glibc 2.24부터는 &lt;code&gt;CLONE_VM&lt;/code&gt;으로 페이지 테이블 복사까지 피함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;자식이 할 수 있는 준비 작업이 라이브러리가 지원하는 항목으로 제한되고, 내부 구현이 libc와 그 버전에 따라 다름&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 &lt;code&gt;fork()&lt;/code&gt;도 자식이 &lt;code&gt;execve()&lt;/code&gt; 전에 임의의 함수를 호출해도 된다는 뜻은 아닙니다. &lt;a href=&quot;https://man7.org/linux/man-pages/man2/fork.2.html&quot;&gt;fork(2)&lt;/a&gt; 매뉴얼은 멀티스레드 프로그램에서 &lt;code&gt;fork()&lt;/code&gt;한 자식이 &lt;code&gt;execve()&lt;/code&gt;까지 안전하게 호출할 수 있는 함수를 async-signal-safe 함수로 제한합니다. 다른 스레드는 자식에 복제되지 않지만 그 스레드가 잡고 있던 잠금 상태는 남을 수 있기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;vfork(2) 매뉴얼은 &lt;code&gt;fork()&lt;/code&gt;의 비용을 &quot;부모의 페이지 테이블을 복제하고 고유한 task 구조체를 만드는 시간과 메모리&quot;라고 적었습니다.
이 비용은 부모의 힙이 클수록 커집니다. 뒤의 측정에서 &lt;code&gt;FORK&lt;/code&gt; 방식으로 명령 하나를 실행하는 시간은 256MB 힙에서 16~20ms였지만 8GB 힙에서는 230~240ms로 늘었습니다.
&lt;code&gt;vfork()&lt;/code&gt;의 제약은 &lt;a href=&quot;https://man7.org/linux/man-pages/man2/vfork.2.html&quot;&gt;vfork(2)&lt;/a&gt; 매뉴얼에 더 강하게 적혀 있습니다. 반환값을 담을 &lt;code&gt;pid_t&lt;/code&gt; 변수 외의 데이터를 고치거나, &lt;code&gt;vfork()&lt;/code&gt;를 호출한 함수에서 반환하거나, &lt;code&gt;_exit()&lt;/code&gt;와 &lt;code&gt;execve()&lt;/code&gt; 외의 함수를 부르면 동작이 정의되지 않습니다(undefined behavior). 자식이 부모와 공유하는 메모리와 스택을 고치므로, 부모가 재개된 뒤의 결과를 예측할 수 없기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OpenJDK는 이 세 방식을 차례로 거쳤습니다. 처음에는 &lt;code&gt;fork()&lt;/code&gt;를 썼지만 큰 힙에서 생성 비용이 문제가 되자 JDK 7에서 &lt;code&gt;vfork()&lt;/code&gt;를 기본값으로 바꿨습니다. 그런데 JDK는 &lt;code&gt;vfork()&lt;/code&gt;와 &lt;code&gt;execve()&lt;/code&gt; 사이에서 파일 디스크립터를 닫고 작업 디렉터리를 바꾸는 준비 작업을 했고, 이는 방금 본 &lt;code&gt;vfork()&lt;/code&gt;의 제약을 어기는 것이었습니다. 실행 비용은 줄였지만 안전성을 희생한 선택이었습니다. JDK 13에서 기본값이 된 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;은 이 준비 작업을 부모와 메모리를 공유하지 않는 별도 프로세스로 옮겨서, &lt;code&gt;fork()&lt;/code&gt;의 복사 비용과 &lt;code&gt;vfork()&lt;/code&gt;의 제약 위반을 함께 피합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세 방식은 구현 전용 시스템 속성인 &lt;code&gt;jdk.lang.Process.launchMechanism&lt;/code&gt;에 각각 &lt;code&gt;FORK&lt;/code&gt;, &lt;code&gt;VFORK&lt;/code&gt;, &lt;code&gt;POSIX_SPAWN&lt;/code&gt;으로 대응합니다.
JDK 25의 &lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/unix/classes/java/lang/ProcessImpl.java&quot;&gt;ProcessImpl.java&lt;/a&gt;에 이 세 값이 선언되어 있고, Linux에서는 셋 다 지정할 수 있되 &lt;code&gt;VFORK&lt;/code&gt;에는 경고가 따릅니다.
기본값의 변천은 아래와 같습니다. JDK 27 항목은 정식 출시 결과가 아니라 2026-09-06 현재 개발 버전에 반영된 내용입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;JDK&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Linux에서의 변화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;6&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;fork()&lt;/code&gt; + &lt;code&gt;execve()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;7&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;vfork()&lt;/code&gt;가 기본값이 됨 (&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-6868160&quot;&gt;JDK-6868160&lt;/a&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;12&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;POSIX_SPAWN&lt;/code&gt;을 선택 옵션으로 추가 (&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8212828&quot;&gt;JDK-8212828&lt;/a&gt;). 11.0.4에도 백포트&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;13&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;POSIX_SPAWN&lt;/code&gt;이 기본값이 됨 (&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8213192&quot;&gt;JDK-8213192&lt;/a&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;25&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;VFORK&lt;/code&gt;를 지정하면 deprecated 경고 출력 (&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8357180&quot;&gt;JDK-8357180&lt;/a&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;27&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;개발 버전에서 &lt;code&gt;VFORK&lt;/code&gt; 제거. 지정하면 경고 후 &lt;code&gt;FORK&lt;/code&gt;로 동작 (&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8357089&quot;&gt;JDK-8357089&lt;/a&gt;, &lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8357090&quot;&gt;CSR&lt;/a&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;VFORK&lt;/code&gt;가 없어진 사정과 남은 설정의 정리는 이 장의 마지막 절에서 다룹니다.
JDK 13부터 기본값인 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;이 준비 작업을 별도 프로세스로 옮기는 구조는 다음 절에서 살펴봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;posix_spawn의_동작&quot;&gt;POSIX_SPAWN의 동작&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;posix_spawn()&lt;/code&gt;은 자식 생성과 &lt;code&gt;execve()&lt;/code&gt;, 그 사이의 준비 작업을 한 묶음으로 제공합니다.
Windows의 &lt;code&gt;CreateProcess()&lt;/code&gt;와 유사합니다.
JDK는 이 함수로 &lt;code&gt;jspawnhelper&lt;/code&gt;라는 작은 실행 파일을 먼저 띄우고, &lt;code&gt;jspawnhelper&lt;/code&gt;가 파이프로 넘겨받은 설정에 따라 파일 디스크립터와 작업 디렉터리를 정리한 뒤 실제 명령을 다시 &lt;code&gt;execve()&lt;/code&gt;합니다. 이 구조는 JDK 25 소스의 &lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/unix/native/libjava/ProcessImpl_md.c&quot;&gt;ProcessImpl_md.c&lt;/a&gt; 상단 주석에 설명되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;helper를 한 번 더 거치는 이유는 앞 절에서 본 안전 문제 때문입니다.
파일 디스크립터 닫기와 작업 디렉터리 변경 같은 준비 작업을 첫 번째 &lt;code&gt;execve()&lt;/code&gt;로 실행된 jspawnhelper, 즉 부모와 메모리를 공유하지 않는 별도 프로세스 안에서 하면 &lt;code&gt;vfork()&lt;/code&gt;의 제약을 어기지 않습니다.
같은 주석은 이를 &quot;준비 작업을 첫 exec 뒤로 옮겨 취약한 시간 창을 줄인다&quot;고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;프로그램이 호출하는 시스템 콜을 순서대로 기록하는 도구인 strace로 보면 &lt;code&gt;execve&lt;/code&gt;가 두 번 나옵니다.
아래는 &lt;code&gt;echo hello&lt;/code&gt;를 실행하는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/java-external-process/ProcessRunner.java&quot;&gt;ProcessRunner.java&lt;/a&gt;를 기본 설정으로 실행했을 때의 시스템 콜 중 프로세스 생성과 관련된 부분입니다. 경로는 줄였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;$ strace -f -e trace=clone,clone3,vfork,execve -o trace.txt java ProcessRunner
$ grep -v CLONE_THREAD trace.txt | grep -E &apos;clone|vfork|execve&apos;
285583 clone3({flags=CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND, exit_signal=SIGCHLD, stack=..., stack_size=0x9000}, 88 &amp;lt;unfinished ...&amp;gt;
285603 execve(&quot;$JAVA_HOME/lib/jspawnhelper&quot;, [&quot;$JAVA_HOME/lib/jspawnhelper&quot;, &quot;25+36-LTS&quot;, &quot;10:11:13&quot;], ...) = 0
285603 execve(&quot;/usr/bin/echo&quot;, [&quot;echo&quot;, &quot;hello&quot;], ...) = 0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 실행에서는 &lt;code&gt;vfork()&lt;/code&gt; 시스템 콜이 나오지 않았습니다.
glibc 2.39의 &lt;code&gt;posix_spawn()&lt;/code&gt;이 &lt;code&gt;clone3&lt;/code&gt; 시스템 콜에 &lt;code&gt;CLONE_VM&lt;/code&gt;과 &lt;code&gt;CLONE_VFORK&lt;/code&gt; 플래그를 주어 자식을 생성했습니다.
환경에 따라 &lt;code&gt;clone()&lt;/code&gt; 시스템 콜을 쓸 수도 있습니다. glibc 2.39의 &lt;a href=&quot;https://github.com/bminor/glibc/blob/glibc-2.39/sysdeps/unix/sysv/linux/spawni.c&quot;&gt;posix_spawn 구현&lt;/a&gt;은 &lt;code&gt;clone3&lt;/code&gt; 호출이 &lt;code&gt;ENOSYS&lt;/code&gt;나 &lt;code&gt;EINVAL&lt;/code&gt;로 실패하면 &lt;code&gt;clone()&lt;/code&gt;으로 대체합니다.
&lt;code&gt;CLONE_VM&lt;/code&gt;은 자식이 부모의 주소 공간을 그대로 공유한다는 뜻이므로, 힙 매핑과 페이지 테이블을 복사하지 않습니다.
&lt;code&gt;CLONE_VFORK&lt;/code&gt;는 자식이 &lt;code&gt;execve()&lt;/code&gt;하거나 끝날 때까지 부모의 호출 스레드를 멈춥니다.
&lt;code&gt;CLONE_VM&lt;/code&gt; 덕분에 생성 비용은 힙 크기와 거의 무관합니다.
뒤의 측정에서 이를 확인합니다. 같은 프로그램을 &lt;code&gt;-Djdk.lang.Process.launchMechanism=FORK&lt;/code&gt;로 실행하면 &lt;code&gt;CLONE_VM&lt;/code&gt; 플래그 없이 &lt;code&gt;clone&lt;/code&gt; 시스템 콜을 불러 주소 공간을 복제하고, &lt;code&gt;jspawnhelper&lt;/code&gt; 없이 곧바로 &lt;code&gt;/usr/bin/echo&lt;/code&gt;를 &lt;code&gt;execve&lt;/code&gt;합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man3/posix_spawn.3.html&quot;&gt;posix_spawn(3)&lt;/a&gt; 매뉴얼에 따르면 glibc 2.24부터 &lt;code&gt;posix_spawn()&lt;/code&gt;은 &lt;code&gt;CLONE_VM&lt;/code&gt;과 &lt;code&gt;CLONE_VFORK&lt;/code&gt; 플래그로 &lt;code&gt;clone()&lt;/code&gt;을 호출합니다.
자식 프로세스에 별도 스택을 주고, 생성 과정에서 시그널을 차단하고 자식의 핸들러를 정리해 부모 스택과 시그널 처리에 관한 위험을 줄입니다.
JDK 25 소스의 주석도 이 이유로 glibc 2.24 이상의 방식을 가장 좋은 선택으로 설명하며, musl 역시 이 &lt;code&gt;clone()&lt;/code&gt; 방식을 사용해 왔다고 적었습니다.
&lt;code&gt;vfork()&lt;/code&gt; 함수 자체가 glibc에서 없어진 것은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;jspawnhelper&lt;/code&gt;가 별도 실행 파일이라는 점은 JDK를 갱신할 때 문제가 될 수 있습니다.
실행 중인 JVM이 사용하는 JDK 경로의 파일을 덮어쓰면, 메모리에 적재된 JVM 코드와 디스크의 helper 버전이 달라져 외부 명령 실행이 실패할 수 있습니다.
&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8325621&quot;&gt;JDK-8325621&lt;/a&gt;은 자동 업데이트로 생긴 이런 불일치를 배경으로 helper의 버전 검사를 보강했습니다.
JDK를 갱신할 때는 실행 중인 JVM이 쓰는 디렉터리를 덮어쓰지 말고 새 디렉터리에 설치한 뒤, JVM을 재시작하면서 새 경로로 전환하는 편이 좋습니다.
helper 실행 오류에는 권한이나 설치 문제 같은 다른 원인도 있으므로 오류 메시지와 설치 상태를 먼저 확인해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8357090&quot;&gt;JDK-8357090의 CSR&lt;/a&gt;은 &lt;code&gt;posix_spawn()&lt;/code&gt;의 예기치 않은 문제를 우회할 수 있도록 &lt;code&gt;FORK&lt;/code&gt;를 대안으로 둔다고 설명합니다.
&lt;code&gt;jspawnhelper&lt;/code&gt; 관련 오류가 반복되는데 원인을 바로 찾기 어렵다면 JVM 시작 옵션에 &lt;code&gt;-Djdk.lang.Process.launchMechanism=FORK&lt;/code&gt;를 주어 임시 회피할 수 있습니다.
다음 절에서 살펴볼 overcommit 정책에 따른 메모리 부담과 실행 지연을 감수하는 선택이며, 실행 중인 JVM에 즉시 적용하는 설정은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;fork의_메모리_비용과_실행_지연&quot;&gt;FORK의 메모리 비용과 실행 지연&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;FORK&lt;/code&gt;를 선택하면 부모 JVM의 주소 공간 복제 비용을 고려해야 합니다.
&lt;code&gt;fork()&lt;/code&gt;는 부모의 가상 주소 공간을 그대로 복제한 자식 프로세스를 만드는데, Linux는 쓰기 시 복사(copy-on-write)로 실제 페이지 복사는 미루지만 페이지 테이블은 복사합니다.
커널의 메모리 overcommit 정책에 따라 자식이 쓸지도 모르는 메모리를 미리 계산해서 생성을 거절할 수도 있습니다.
자식이 곧바로 &lt;code&gt;execve()&lt;/code&gt;로 작은 프로그램으로 바뀌더라도 이 검사를 거칩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;/proc/sys/vm/overcommit_memory&lt;/code&gt; 파일로 노출되는 Linux 커널 설정 &lt;code&gt;vm.overcommit_memory&lt;/code&gt;의 기본값 0은 &quot;명백한 overcommit만 거절하는&quot; 모드입니다.
커널 5.1의 코드는 이 판정에 여유 메모리, 페이지 캐시, 스왑 여유분 등을 반영했습니다. &lt;code&gt;fork()&lt;/code&gt;가 힙 매핑을 복제할 때 요구하는 크기가 이 계산값을 넘으면 메모리 부족을 뜻하는 오류 코드 &lt;code&gt;ENOMEM&lt;/code&gt;으로 거절될 수 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;커널은 이 판정이 지나치게 엄격해지지 않도록 몇 가지 규칙을 두고 있고, 버전이 올라가면서 보강되기도 했습니다. 두 가지 예를 들면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vm.overcommit_memory=2&lt;/code&gt;로 commit 한도를 엄격히 적용한 서버에서도 &lt;code&gt;fork()&lt;/code&gt; 시점의 commit 검사는 부모의 전체 주소 공간 크기를 무조건 더하지는 않습니다. private writable 매핑처럼 커널이 commit 총량에 넣어 세는 매핑의 크기만 비용으로 계산합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;커널 5.2에 들어간 &lt;a href=&quot;https://lkml.iu.edu/hypermail/linux/kernel/1904.1/05420.html&quot;&gt;mm: fix false-positive OVERCOMMIT_GUESS failures&lt;/a&gt; 패치는 모드 0의 판정을 &quot;요청 크기가 전체 RAM과 스왑의 합을 넘는지&quot;로 단순화했습니다. 여유 메모리가 적다는 이유만으로 큰 매핑의 복제를 거절하던 조건이 완화된 것입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그럼에도 &lt;code&gt;FORK&lt;/code&gt;가 항상 성공하는 것은 아닙니다. 모드 2에서는 Java 힙처럼 큰 익명 private writable 매핑이 비용에 포함되므로 남은 commit 한도를 넘으면 프로세스 생성이 실패하고, 모드 0에서도 페이지 테이블 등 커널 메모리의 실제 할당은 실패할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;생성이 거절되지 않더라도 시간 비용은 남습니다.
overcommit 판정이 완화됐어도 &lt;code&gt;fork()&lt;/code&gt;가 페이지 테이블을 복사한다는 사실은 변하지 않았습니다.
실제로 접근한 힙 영역과 페이지 크기 등에 따라 복사 비용이 달라집니다.
반면 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;은 &lt;code&gt;CLONE_VM&lt;/code&gt;으로 주소 공간을 공유하므로 이 복사가 없습니다.
힙을 미리 할당한 상태에서 &lt;code&gt;true&lt;/code&gt; 명령을 30번 실행하는 데 걸린 시간을 실행 방식별로 쟀습니다.
측정 코드는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/java-external-process/SpawnBench.java&quot;&gt;SpawnBench.java&lt;/a&gt;이고, &lt;code&gt;-Xms&lt;/code&gt;와 &lt;code&gt;-Xmx&lt;/code&gt;를 같은 값으로 맞추고 &lt;code&gt;-XX:+AlwaysPreTouch&lt;/code&gt; 옵션으로 힙을 미리 접근한 뒤 측정했습니다.
각 실행에서 5회 예열 후 30회의 &lt;code&gt;start().waitFor()&lt;/code&gt; 평균을 구했으므로, 순수한 생성 시간뿐 아니라 명령 실행과 종료 대기도 포함합니다.
아래는 글 작성 당시 두 번 실행한 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;힙 크기&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;POSIX_SPAWN (ms/회)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;FORK (ms/회)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;256MB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.5, 1.2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15.9, 20.2&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2GB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.6, 1.9&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;120.8, 140.7&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;8GB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2.0, 2.1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;243.5, 231.2&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 측정에서 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;은 약 1~2ms 범위였고, &lt;code&gt;FORK&lt;/code&gt;는 힙이 커질수록 느려져 8GB에서는 100배 넘게 차이가 났습니다.
힙 크기에 정확히 비례하거나 모든 환경에서 이 배율이 나온다는 뜻은 아닙니다.
2026-09-06 재측정에서도 같은 경향을 확인했지만 절대 시간은 달랐습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;FORK&lt;/code&gt; 방식에서 주소 공간을 복제하는 Linux 6.17의 &lt;code&gt;dup_mmap()&lt;/code&gt;은 부모 프로세스의 주소 공간 잠금(mmap_lock)을 쓰기 모드로 잡은 상태에서 매핑과 페이지 테이블을 복제합니다.
그동안 이 잠금이 필요한 메모리 매핑 변경을 시도하는 다른 스레드는 기다려야 합니다.
요청 처리 중 외부 명령을 동기 실행하는 웹 애플리케이션이라면 이 비용이 응답 시간에 더해질 수 있습니다.
&lt;code&gt;jspawnhelper&lt;/code&gt; 문제를 임시로 피하는 경우가 아니라면, 지금은 이런 단점을 감수하면서 기본값을 &lt;code&gt;FORK&lt;/code&gt;로 바꿀 만한 이유가 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jvm_업그레이드_때_정리할_vfork_설정&quot;&gt;JVM 업그레이드 때 정리할 VFORK 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JVM 버전을 25 이상으로 업그레이드할 때 &lt;code&gt;-Djdk.lang.Process.launchMechanism=VFORK&lt;/code&gt; 설정이 남아 있다면 제거하는 편이 좋습니다.
이 설정이 남아 있을 만한 자리는 JVM 실행 스크립트, Dockerfile의 &lt;code&gt;JAVA_OPTS&lt;/code&gt;, 애플리케이션 서버의 기동 옵션입니다.
JDK 25에서는 경고가 나오고, JDK 27 개발 버전에 반영된 변경에서는 &lt;code&gt;FORK&lt;/code&gt;로 대체되어 큰 힙에서 실행 지연이 커질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linux에서는 JDK 7부터 12까지 &lt;code&gt;vfork()&lt;/code&gt;가 기본이었으므로, JDK 13에서 기본값이 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;으로 바뀔 때 이전 동작을 유지하려고 이 속성을 명시적으로 적어 둔 경우가 있습니다.
당시 glibc가 2.24보다 오래된 환경이었더라도 이 값을 지정할 필요는 없었습니다.
&lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-13%2B33/src/java.base/unix/native/libjava/ProcessImpl_md.c&quot;&gt;JDK 13 소스&lt;/a&gt;의 주석에 따르면 glibc 2.4~2.23의 &lt;code&gt;posix_spawn()&lt;/code&gt;은 호출 인자에 따라 &lt;code&gt;fork()&lt;/code&gt;와 &lt;code&gt;vfork()&lt;/code&gt; 중 하나를 고르는데, JDK의 호출 방식은 &lt;code&gt;vfork()&lt;/code&gt;를 쓰는 조건에 해당합니다.
glibc 2.24부터는 앞에서 본 별도 스택과 시그널 처리를 갖춘 &lt;code&gt;clone()&lt;/code&gt; 기반 구현으로 바뀌었습니다.
지금은 이 값을 지정할 이유가 없으므로 속성을 지우고 기본값으로 돌아가면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;vfork()&lt;/code&gt;를 기본값으로 삼은 JDK 7의 선택은 당시에도 논란거리였습니다.
&lt;code&gt;vfork()&lt;/code&gt;는 표준 함수가 아닙니다.
&lt;a href=&quot;https://man7.org/linux/man-pages/man2/vfork.2.html&quot;&gt;vfork(2)&lt;/a&gt; 매뉴얼의 STANDARDS 항목은 &quot;None&quot;이고, HISTORY 항목은 3.0BSD에서 등장한 이 함수가 POSIX.1-2001에 OBSOLETE로 표시됐다가 POSIX.1-2008에서 명세가 삭제됐다고 적습니다.
4.4BSD는 &lt;code&gt;vfork()&lt;/code&gt;를 아예 &lt;code&gt;fork()&lt;/code&gt;와 같은 것으로 만들었고, Linux도 2.2.0-pre6 무렵까지는 &lt;code&gt;fork()&lt;/code&gt;와 같게 동작하다가 2.2.0-pre9부터 독립된 시스템 콜이 됐습니다.
그래도 없어진 쪽은 커널의 &lt;code&gt;vfork()&lt;/code&gt;가 아니라 JDK의 &lt;code&gt;VFORK&lt;/code&gt; 모드입니다.
표준에서 빠졌기 때문도 커널이 바뀌었기 때문도 아니라, 앞에서 본 대로 JDK 자신이 &lt;code&gt;vfork()&lt;/code&gt;와 &lt;code&gt;execve()&lt;/code&gt; 사이에서 하던 작업이 안전하지 않아서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 25에서 이 옵션을 지정하면 아래 경고가 표준 오류에 출력됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;$ java -Djdk.lang.Process.launchMechanism=VFORK ProcessRunner
VFORK MODE DEPRECATED
The VFORK launch mechanism has been deprecated for being dangerous.
It will be removed in a future java version. Either remove the
jdk.lang.Process.launchMechanism property (preferred) or use FORK mode
instead (-Djdk.lang.Process.launchMechanism=FORK).&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글의 예제와 측정은 JDK 25 기준입니다.
구버전 JDK에 적용할 때는 실행 방식과 API 지원 범위를 따로 확인해야 합니다.
예를 들어 OpenJDK 11u는 11.0.4부터 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;을 옵션으로 지정할 수 있지만 기본값은 &lt;code&gt;vfork()&lt;/code&gt;이고, OpenJDK 8u의 Linux 구현은 2026-09-06 현재 이 옵션을 지원하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;외부 프로세스 실행 코드는 출력 처리, 시간제한, 종료 정책을 함께 설계해야 합니다. 출력을 가공할 필요가 없으면 &lt;code&gt;Redirect.DISCARD&lt;/code&gt;나 &lt;code&gt;inheritIO()&lt;/code&gt;를 쓰고, 표준 출력과 표준 오류를 따로 받아야 하면 두 스트림을 동시에 읽어야 합니다. 표준 입력을 읽는 명령에는 입력을 다 쓴 뒤 스트림을 닫아 EOF를 받게 해야 합니다. 시간제한 뒤에는 종료 요청뿐 아니라 강제 종료 여부와 자원 정리까지 챙겨야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;zt-exec과 Commons Exec은 이 코드를 줄여 줍니다. 출력과 종료 코드의 기본값, 시간 초과를 알리는 방식, 의존성을 비교해 프로젝트에 맞는 쪽을 고르면 됩니다. 두 라이브러리 모두 기본 &lt;code&gt;destroy()&lt;/code&gt;만으로 강제 종료까지 보장하지는 않으며, 후손 정리에는 같은 그룹에 남은 프로세스에 시그널을 보내는 방법이나 cgroup 단위의 종료를 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linux의 JDK 25에서는 기본값인 &lt;code&gt;POSIX_SPAWN&lt;/code&gt;을 유지하는 편이 무난합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK 문서&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Process.html&quot;&gt;Process (Java SE 25)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/ProcessBuilder.html&quot;&gt;ProcessBuilder (Java SE 25)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Runtime.html&quot;&gt;Runtime (Java SE 25)&lt;/a&gt;: &lt;code&gt;exec(String)&lt;/code&gt; 계열의 deprecation 사유&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/Runtime.java&quot;&gt;Runtime.java (JDK 25+36)&lt;/a&gt;: &lt;code&gt;exec()&lt;/code&gt;의 &lt;code&gt;ProcessBuilder&lt;/code&gt; 위임 구현&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/unix/native/libjava/ProcessImpl_md.c&quot;&gt;ProcessImpl_md.c (JDK 25+36)&lt;/a&gt;: 실행 방식별 장단점과 glibc, musl의 &lt;code&gt;posix_spawn()&lt;/code&gt; 구현을 설명한 주석&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;라이브러리&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zeroturnaround/zt-exec&quot;&gt;ZT Process Executor (zt-exec)&lt;/a&gt;: README의 사용 예&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zeroturnaround/zt-exec/blob/main/CHANGELOG.md&quot;&gt;ZT Process Executor CHANGELOG&lt;/a&gt;: 1.13.0의 변경 사항과 릴리스 날짜&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://commons.apache.org/proper/commons-exec/&quot;&gt;Apache Commons Exec&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://commons.apache.org/proper/commons-exec/changes.html&quot;&gt;Apache Commons Exec Changes&lt;/a&gt;: 1.4.0 이후의 builder API와 &lt;code&gt;Duration&lt;/code&gt; 도입&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenJDK 이슈&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-6868160&quot;&gt;JDK-6868160 (process) Use vfork, not fork, on Linux to avoid swap exhaustion&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8212828&quot;&gt;JDK-8212828 (process) Provide a way for Runtime.exec to use posix_spawn on linux&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8213192&quot;&gt;JDK-8213192 (process) Change the Process launch mechanism default on Linux to be posix_spawn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8357180&quot;&gt;JDK-8357180 Deprecate VFORK launch mechanism from Process implementation (linux)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8357090&quot;&gt;JDK-8357090 Remove VFORK launch mechanism from Process implementation (linux)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/commit/ca3fe721ba23a1304089b71c1b58940f16a0d053&quot;&gt;JDK-8357089 구현 커밋&lt;/a&gt;: JDK 27 개발 버전의 &lt;code&gt;VFORK&lt;/code&gt; 제거와 &lt;code&gt;FORK&lt;/code&gt; 대체&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/486&quot;&gt;JEP 486: Permanently Disable the Security Manager&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux 매뉴얼과 커널&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man7/pipe.7.html&quot;&gt;pipe(7)&lt;/a&gt;: 파이프 용량&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man1/setsid.1.html&quot;&gt;setsid(1)&lt;/a&gt;: 그룹 리더가 아닐 때 fork 없이 실행하는 동작&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/setsid.2.html&quot;&gt;setsid(2)&lt;/a&gt;, &lt;a href=&quot;https://manpages.ubuntu.com/manpages/noble/man1/kill.1.html&quot;&gt;kill(1) (procps-ng)&lt;/a&gt;, &lt;a href=&quot;https://man7.org/linux/man-pages/man2/kill.2.html&quot;&gt;kill(2)&lt;/a&gt;: 새 세션 생성, 음수 PID로 그룹에 시그널 보내기, 시그널 0의 존재 확인&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/PR_SET_CHILD_SUBREAPER.2const.html&quot;&gt;PR_SET_CHILD_SUBREAPER(2const)&lt;/a&gt;: subreaper의 재부모화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/cgroup-v2.html&quot;&gt;Control Group v2&lt;/a&gt;: 프로세스 이동 권한과 &lt;code&gt;cgroup.kill&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html&quot;&gt;systemd-run(1)&lt;/a&gt;, &lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/latest/systemd.kill.html&quot;&gt;systemd.kill(5)&lt;/a&gt;: scope 단위 실행과 &lt;code&gt;KillMode&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/vfork.2.html&quot;&gt;vfork(2)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man3/posix_spawn.3.html&quot;&gt;posix_spawn(3)&lt;/a&gt;: glibc 2.24 이후의 구현&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.kernel.org/doc/html/latest/mm/overcommit-accounting.html&quot;&gt;Overcommit Accounting&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/torvalds/linux/blob/v5.2/mm/util.c&quot;&gt;Linux 5.2 mm/util.c&lt;/a&gt;: &lt;code&gt;__vm_enough_memory()&lt;/code&gt;의 모드 0 판정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/torvalds/linux/blob/v6.17/mm/mmap.c&quot;&gt;Linux 6.17 mm/mmap.c&lt;/a&gt;: &lt;code&gt;dup_mmap()&lt;/code&gt;의 잠금과 페이지 테이블 복제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://lkml.iu.edu/hypermail/linux/kernel/1904.1/05420.html&quot;&gt;mm: fix false-positive OVERCOMMIT_GUESS failures&lt;/a&gt;: 커널 5.2의 휴리스틱 변경 패치&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.infoworld.com/article/2157336/when-runtime-exec-won-t.html&quot;&gt;When Runtime.exec() won&amp;#8217;t&lt;/a&gt;: 2000년에 나온 Michael Daconta의 글&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1113548&quot;&gt;Java에서 외부 프로세스를 실행할 때&lt;/a&gt; (NAVER D2, 2015): &lt;code&gt;fork()&lt;/code&gt;의 메모리 문제와 JDK 7의 &lt;code&gt;vfork()&lt;/code&gt; 도입 배경&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/java-external-process&quot;&gt;이 글의 예제 코드&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>VO vs DTO: 정의와 용어 혼용의 역사</title>
      <link>https://blog.benelog.net//vo-vs-dto.html</link>
      <pubDate>Sat, 29 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">vo-vs-dto.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#value_object의_정의&quot;&gt;Value Object의 정의&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#불변성의_위상_정의_요건_vs_바람직한_설계&quot;&gt;불변성의 위상: 정의 요건 vs 바람직한 설계&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dtodata_transfer_object의_정의&quot;&gt;DTO(Data Transfer Object)의 정의&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#core_j2ee_patterns_초판의_vo와_개정판의_to&quot;&gt;Core J2EE Patterns 초판의 VO와 개정판의 TO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#데이터_운반_객체를_vo라고_부를_때의_비용&quot;&gt;데이터 운반 객체를 VO라고 부를 때의 비용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#value_object&quot;&gt;Value Object&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#transfer_object_dto&quot;&gt;Transfer Object / DTO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#관련_발표_자료&quot;&gt;관련 발표 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;getter/setter만 있는, 값을 실어 나르는 객체를 VO(Value Object)라고 부르는 사례를 실무에서 종종 접합니다. 프로세스 사이의 원격 호출에서 데이터를 운반하는 역할이라면 그 객체는 DTO(Data Transfer Object)라고 부르는 편이 혼란의 여지가 적습니다. Value Object는 Martin Fowler의 글과 Eric Evans의 DDD(Domain-Driven Design)에서 이미 다른 뜻으로 정의한 용어이기 때문입니다. 이 글에서는 두 패턴의 정의와 용어가 뒤섞인 역사를 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;value_object의_정의&quot;&gt;Value Object의 정의&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Value Object는 식별성(identity)이 없고, 담고 있는 값으로 동등성(equality)을 판단하는 객체입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;식별성은 속성 값과 상관없이 한 객체를 다른 객체와 구별해 주는 성질입니다. 회원은 이름이나 주소가 바뀌어도 같은 회원이고, 이름과 주소가 모두 같은 두 회원도 서로 다른 사람입니다. 이런 객체는 회원 번호 같은 식별자로 구별하며, DDD에서는 ENTITY라고 부릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;동등성은 두 객체를 같은 것으로 볼지 판단하는 기준입니다. Value Object는 이 기준을 식별자가 아니라 담고 있는 값에 둡니다. 돈, 색상, 날짜 같은 개념이 대표적인 예입니다. 예를 들어 Java에서 시간선 위의 한 시점을 나타내는 &lt;code&gt;Instant&lt;/code&gt;는 서로 다른 방법으로 따로 만들었더라도 같은 시점을 가리키면 &lt;code&gt;equals()&lt;/code&gt;로 비교했을 때 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Instant fromText = Instant.parse(&quot;2026-09-24T20:00:00Z&quot;);
Instant fromSeoulTime = ZonedDateTime.of(2026, 9, 25, 5, 0, 0, 0, ZoneId.of(&quot;Asia/Seoul&quot;))
    .toInstant();

assertThat(fromText).isEqualTo(fromSeoulTime); // UTC 24일 20시와 서울 25일 5시는 같은 시점&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 세 자료에서 이 정의를 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/bliki/ValueObject.html&quot;&gt;Martin Fowler의 글&lt;/a&gt;: 속성 값이 같아서 같다고 보는 객체. x, y 좌표로 이루어진 점(point)을 예로 들어 설명합니다.&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Objects that are equal due to the value of their properties, in this case their x and y coordinates, are called value objects.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;속성 값, 이 예에서는 x와 y 좌표 값이 같아서 같은 객체를 value object라고 부른다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Value_object&quot;&gt;Wikipedia&lt;/a&gt;: 동등성이 식별성에 기반하지 않고, 같은 값을 가지면 같은 것으로 보는 객체&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects&quot;&gt;Microsoft의 .NET 아키텍처 문서&lt;/a&gt;: 식별성이 없는 객체&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eric Evans도 같은 관점입니다. Evans가 &lt;strong&gt;Domain-Driven Design&lt;/strong&gt;의 패턴 정의를 요약해 공개한 &lt;a href=&quot;https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf&quot;&gt;Domain-Driven Design Reference&lt;/a&gt;(2015)의 Value Objects 항목은 문제를 서술하는 부분에서 많은 객체에 개념적 식별성이 없다고 전제하고, 속성과 로직에만 관심이 있는 모델 요소를 value object로 분류하라고 말합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Some objects describe or compute some characteristic of a thing. Many objects have no conceptual identity. &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When you care only about the attributes and logic of an element of the model, classify it as a value object. Make it express the meaning of the attributes it conveys and give it related functionality.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 객체는 사물의 특성을 기술하거나 계산한다. 많은 객체에는 개념적 식별성이 없다. &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러므로:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모델 요소의 속성과 로직에만 관심이 있다면 그것을 value object로 분류하라. Value object가 담은 속성의 의미를 드러내게 하고 관련 기능을 부여하라.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Eric Evans&lt;br&gt;
&lt;cite&gt;Domain-Driven Design Reference: Value Objects&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 코드에서는 값 개념을 표현하는 클래스에 동등성의 기준이 되는 속성으로 &lt;code&gt;equals()&lt;/code&gt;와 &lt;code&gt;hashCode()&lt;/code&gt;를 구현하면 Value Object가 됩니다. 이때 지켜야 할 두 메서드의 규약은 Joshua Bloch의 &lt;strong&gt;Effective Java&lt;/strong&gt; 3판 Item 10과 Item 11에 정리되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Item 10: &lt;code&gt;equals()&lt;/code&gt;의 일반 규약&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;반사성(reflexive): &lt;code&gt;x.equals(x)&lt;/code&gt;는 &lt;code&gt;true&lt;/code&gt;를 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;대칭성(symmetric): &lt;code&gt;x.equals(y)&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt;이면 &lt;code&gt;y.equals(x)&lt;/code&gt;도 &lt;code&gt;true&lt;/code&gt;를 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;추이성(transitive): &lt;code&gt;x.equals(y)&lt;/code&gt;와 &lt;code&gt;y.equals(z)&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt;이면 &lt;code&gt;x.equals(z)&lt;/code&gt;도 &lt;code&gt;true&lt;/code&gt;를 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;일관성(consistent): 비교에 쓰는 정보가 바뀌지 않는 한 &lt;code&gt;x.equals(y)&lt;/code&gt;는 여러 번 호출해도 같은 결과를 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;null 아님(non-null): &lt;code&gt;x.equals(null)&lt;/code&gt;은 &lt;code&gt;false&lt;/code&gt;를 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Item 11: &lt;code&gt;equals()&lt;/code&gt;를 재정의하면 &lt;code&gt;hashCode()&lt;/code&gt;도 재정의&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;equals()&lt;/code&gt; 비교에 쓰는 정보가 바뀌지 않는 한 &lt;code&gt;hashCode()&lt;/code&gt;는 여러 번 호출해도 같은 값을 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;equals()&lt;/code&gt;로 같은 두 객체는 같은 &lt;code&gt;hashCode()&lt;/code&gt; 값을 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;equals()&lt;/code&gt;로 다른 두 객체가 서로 다른 &lt;code&gt;hashCode()&lt;/code&gt; 값을 반환할 필요는 없지만, 다른 값을 반환하면 해시 테이블의 성능이 좋아집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 16부터 정식 기능이 된 record를 쓰면 이 두 메서드를 직접 작성하지 않아도 됩니다. record를 도입한 &lt;a href=&quot;https://openjdk.org/jeps/395&quot;&gt;JEP 395&lt;/a&gt;는 record의 &lt;code&gt;equals()&lt;/code&gt;와 &lt;code&gt;hashCode()&lt;/code&gt;가 모든 구성 요소의 값을 기준으로 자동 생성된다고 명시합니다. 두 record 인스턴스는 타입이 같고 구성 요소의 값이 모두 같을 때 동등합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public record Money(BigDecimal amount, Currency currency) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 클래스의 두 인스턴스는 담고 있는 값이 같으면 동등합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Currency krw = Currency.getInstance(&quot;KRW&quot;);
Money price1 = new Money(BigDecimal.valueOf(10000), krw);
Money price2 = new Money(BigDecimal.valueOf(10000), krw);

assertThat(price1).isEqualTo(price2); // 참조가 달라도 값이 같으면 동등&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 record는 구성 요소 타입의 &lt;code&gt;equals()&lt;/code&gt;를 그대로 쓰므로, 자동 생성된 동등성이 도메인에서 원하는 동등성과 다를 수 있습니다. 예를 들어 &lt;code&gt;BigDecimal&lt;/code&gt;의 &lt;code&gt;equals()&lt;/code&gt;는 scale까지 비교하므로 &lt;code&gt;new BigDecimal(&quot;10000&quot;)&lt;/code&gt;과 &lt;code&gt;new BigDecimal(&quot;10000.0&quot;)&lt;/code&gt;으로 만든 &lt;code&gt;Money&lt;/code&gt;는 서로 다른 값이 됩니다. 이럴 때는 생성자에서 값을 정규화하거나 &lt;code&gt;equals()&lt;/code&gt;를 직접 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 언어와 JVM에도 식별성 없는 value object 개념이 도입되고 있습니다. DDD와 Fowler가 말하는 Value Object와는 식별성 없이 값으로 구분된다는 공통점이 있습니다. Project Valhalla의 &lt;a href=&quot;https://openjdk.org/jeps/401&quot;&gt;JEP 401: Value Objects (Preview)&lt;/a&gt;는 불변이고 객체 식별성이 없으며 필드 값으로만 구분되는 value object를 제안합니다. 2026년 8월 현재 이 JEP는 2027년 3월 출시 예정인 JDK 28에 미리보기 기능으로 통합되어 있습니다. &lt;a href=&quot;https://openjdk.org/jeps/169&quot;&gt;JEP 169: Larval State for Value Objects&lt;/a&gt;는 이러한 불변 value object의 임시 가변 상태를 다루는 별도의 Draft 제안입니다. 다만 DDD의 식별성은 도메인에서 개념적으로 구별할 필요가 있는가의 문제이고, Valhalla가 없애는 식별성은 JVM이 객체를 구분하는 런타임 성질이라서 두 value object는 같은 개념이 아닙니다. 그래도 어느 쪽에서도 value object를 getter/setter만 가진 객체라는 의미로 사용하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한편 실무에서는 앞의 정의와 무관하게 VO를 데이터를 담는 객체(data holder)라는 넓은 의미로 해석해서, getter/setter만 가진 운반용 객체를 VO라고 부르는 관례도 퍼져 있습니다. 이 관례를 확산시킨 대표적인 출처는 뒤의 Core J2EE Patterns 절에서 살펴봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;불변성의_위상_정의_요건_vs_바람직한_설계&quot;&gt;불변성의 위상: 정의 요건 vs 바람직한 설계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 책과 글이 Value Object를 완전한 불변 객체로 만들라고 권고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Fowler는 &lt;strong&gt;Patterns of Enterprise Application Architecture&lt;/strong&gt; 486쪽에서 Value Object를 불변으로 만드는 것이 매우 좋은 방법이라고(&quot;it&amp;#8217;s a very good idea to make them immutable&quot;) 권고합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fowler의 Value Object 글도 마찬가지입니다. &lt;a href=&quot;https://web.archive.org/web/20110104080034/http://martinfowler.com/bliki/ValueObject.html&quot;&gt;2016년 개정 전 버전&lt;/a&gt;에서는 value object를 완전히 불변으로 만드는 것을 일반적인 경험칙으로 제시했고, 개정된 현재 버전에서도 &quot;value objects should be immutable&quot;을 중요한 규칙으로 제시합니다.&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A general heuristic is that value objects should be entirely immutable.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일반적인 경험칙은 value object를 완전히 불변으로 만들어야 한다는 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eric Evans는 앞에서 인용한 DDD Reference의 Value Objects 항목에서 value object를 불변으로 다루라는 설계 지침을 제시합니다.&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Treat the value object as immutable. Make all operations Side-effect-free Functions that don&amp;#8217;t depend on any mutable state. Don&amp;#8217;t give a value object any identity and avoid the design complexities necessary to maintain entities.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Value object를 불변으로 다루라. 모든 연산을 가변 상태에 의존하지 않는 부수 효과 없는 함수(Side-effect-free Function)로 만들라. Value object에는 식별성을 부여하지 말고, entity를 유지하는 데 필요한 설계 복잡성을 피하라.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Joshua Bloch는 &lt;strong&gt;Effective Java&lt;/strong&gt; 3판의 Item 17 &quot;Minimize mutability&quot;에서 Value Object에 한정하지 않고 클래스 일반에 대해 가변성을 최소화하고 가능하면 불변으로 만들라고 권합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;VO가 불변이면 여러 객체가 같은 인스턴스를 공유해도 별칭 문제(aliasing bug)가 생기지 않습니다. 별칭 문제는 한쪽에서 값을 바꾸면 같은 인스턴스를 참조하는 다른 쪽의 값까지 바뀌는 문제입니다. Java의 &lt;code&gt;java.util.Date&lt;/code&gt;와 &lt;code&gt;Calendar&lt;/code&gt;는 값의 성격을 가진 객체인데도 가변으로 설계되어서 이런 버그의 원인이 되었습니다. 예를 들어 두 객체가 회의 시작 시각을 담은 &lt;code&gt;Date&lt;/code&gt; 인스턴스 하나를 공유할 때 한쪽에서 &lt;code&gt;setTime()&lt;/code&gt;을 호출하면 다른 쪽의 시작 시각도 바뀝니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Date 클래스의 부작용 사례&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;record Meeting(String title, Date start) {
}

Date start = new Date();
Meeting review = new Meeting(&quot;설계 리뷰&quot;, start);
Meeting retro = new Meeting(&quot;회고&quot;, start);

long oneHourLater = start.getTime() + Duration.ofHours(1).toMillis();
review.start().setTime(oneHourLater); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;

assertThat(retro.start().getTime()).isEqualTo(oneHourLater); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;설계 리뷰만 1시간 미루려고 함&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;회고 시작 시각도 바뀜&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Meeting&lt;/code&gt;을 record로 선언하는 것만으로는 이 문제를 막을 수 없습니다. record는 필드에 다른 인스턴스를 대입하지 못하게 할 뿐이고, 필드가 참조하는 &lt;code&gt;Date&lt;/code&gt;의 내부 상태는 여전히 바꿀 수 있기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 8의 &lt;code&gt;java.time&lt;/code&gt; 패키지가 &lt;code&gt;LocalDate&lt;/code&gt;, &lt;code&gt;Instant&lt;/code&gt; 같은 날짜·시간 클래스를 모두 불변으로 만든 것도 이 교훈을 반영한 결과입니다. 시작 시각을 시간대 없는 날짜·시각인 &lt;code&gt;LocalDateTime&lt;/code&gt;으로 표현하면 &lt;code&gt;plusHours()&lt;/code&gt;가 기존 인스턴스를 고치지 않고 새 인스턴스를 반환하므로, 인스턴스를 공유해도 다른 회의의 시작 시각은 바뀌지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;record Meeting(String title, LocalDateTime start) {
}

LocalDateTime start = LocalDateTime.of(2026, 9, 25, 14, 0);
Meeting review = new Meeting(&quot;설계 리뷰&quot;, start);
Meeting retro = new Meeting(&quot;회고&quot;, start);

Meeting delayedReview = new Meeting(review.title(), review.start().plusHours(1));

assertThat(delayedReview.start()).isEqualTo(LocalDateTime.of(2026, 9, 25, 15, 0));
assertThat(retro.start()).isEqualTo(start); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;회고 시작 시각은 그대로&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 Fowler와 Evans의 문장을 자세히 보면 불변성은 VO의 정의가 아니라 지침으로 나옵니다. Fowler는 별칭 문제를 피하기 위한 규칙으로, Evans는 명령형 설계 지침으로 불변성을 제시합니다. Fowler가 Value Object의 정의로 제시하는 성질은 값에 의한 동등성입니다. 앞에서 인용한 &quot;value objects should be immutable&quot;도 전체 문장을 보면 별칭 문제를 피하기 위해 따르는 규칙으로 등장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To avoid aliasing bugs I follow a simple but important rule: value objects should be immutable.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;별칭 버그를 피하려고 나는 단순하지만 중요한 규칙 하나를 따른다. Value object는 불변이어야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;Value Object&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 글에서 C#의 struct처럼 언어가 대입할 때마다 값을 복사하는 방식으로도 별칭 문제를 피할 수 있다고 대안까지 언급합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;While immutability is my favorite technique to avoid aliasing bugs, it&amp;#8217;s also possible to avoid them by ensuring assignments always make a copy. Some languages provide this ability, such as structs in C#.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;별칭 버그를 피하는 방법으로 내가 가장 좋아하는 것은 불변성이지만, 대입할 때마다 항상 복사본을 만들도록 보장해서 피할 수도 있다. C#의 struct처럼 이런 기능을 제공하는 언어도 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;Value Object&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Evans는 불변성을 명령형 문장으로 썼습니다. 앞에서 인용한 DDD Reference의 해법 부분에서 value object로 분류하는 기준은 모델 요소의 속성과 로직에만 관심이 있는가입니다. 그 뒤에 이어지는 &quot;Treat the value object as immutable.&quot;은 그렇게 분류한 객체를 불변으로 다루라는 명령형 문장이고, 바로 뒤의 문장도 모든 연산을 부수 효과 없는 함수로 만들라는 명령형 문장입니다. 저는 이 문장들을 분류 기준이 아니라, 분류한 객체를 다루는 설계 지침으로 읽습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ward Cunningham의 wiki에 있는 &lt;a href=&quot;https://wiki.c2.com/?ValueObjectsShouldBeImmutable&quot;&gt;ValueObjectsShouldBeImmutable&lt;/a&gt; 페이지에 Fowler가 남긴 글은 정의와 지침의 구분을 더 분명하게 보여 줍니다. 새로 설계하는 객체에는 상태를 바꾸는 메서드를 두지 말라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So if you design an object that should be a value object, don&amp;#8217;t provide any methods that change its state, ie make it immutable.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러니 value object여야 하는 객체를 설계한다면 상태를 바꾸는 메서드를 제공하지 말라. 즉, 불변으로 만들라.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;c2 wiki: ValueObjectsShouldBeImmutable&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 이미 가변으로 만들어진 Value Object에 대해서는 다음과 같이 말합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you are using a ValueObject that is mutable, treat it like it is immutable. You may not realize why, but you will save a lot of time and money.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가변인 ValueObject를 쓰고 있다면 불변인 것처럼 다루라. 이유를 깨닫지 못할 수도 있지만, 많은 시간과 돈을 아끼게 될 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;c2 wiki: ValueObjectsShouldBeImmutable&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;가변인 ValueObject를 쓰고 있다면&quot;이라는 가정 자체가 불변이 아닌 Value Object의 존재를 전제합니다. 페이지 이름도 MustBeImmutable이 아니라 ShouldBeImmutable입니다. &lt;a href=&quot;https://dictionary.cambridge.org/dictionary/english/should&quot;&gt;Cambridge Dictionary&lt;/a&gt;는 should의 첫 번째 뜻을 다음과 같이 풉니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;used to say or ask what is the correct or best thing to do&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;무엇이 옳거나 가장 좋은 일인지 말하거나 물을 때 쓴다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Cambridge Dictionary&lt;br&gt;
&lt;cite&gt;should&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 사전은 &lt;a href=&quot;https://dictionary.cambridge.org/dictionary/english/must&quot;&gt;must&lt;/a&gt;를 어떤 일이 일어나는 것이 필요하거나 매우 중요하다는 것을 나타낼 때 쓴다고 풉니다. must가 반드시 그래야 하는 필요를 나타낸다면, should는 그렇게 하는 편이 옳다는 권고를 나타냅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반면 불변성을 정의의 일부로 서술하는 자료도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;앞에서 언급한 Microsoft의 .NET 아키텍처 문서는 value object의 두 가지 주요 특성으로 식별성 없음과 불변성을 나란히 들고, 불변성을 중요한 요건이라고 밝힙니다.&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There are two main characteristics for value objects: They have no identity. They are immutable. The first characteristic was already discussed. Immutability is an important requirement.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Value object의 주요 특성은 두 가지다. 식별성이 없고, 불변이다. 첫 번째 특성은 이미 다루었다. 불변성은 중요한 요건이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Wikipedia는 value object가 불변이어야 한다(should)고 쓰면서, 같은 값으로 생성된 두 value object가 계속 같아야 한다는 암묵적 계약을 위해 불변성이 필요하다(required)고 설명합니다. should를 쓰면서도 불변성을 계약의 전제로 둔다는 점에서 정의에 가까운 위상을 부여합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vaughn Vernon의 &lt;strong&gt;Implementing Domain-Driven Design&lt;/strong&gt;(2013) 6장도 Value Object의 특성을 열거하면서 불변성을 그중 하나로 포함합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;김우근 님의 &lt;strong&gt;자바/스프링 개발자를 위한 실용주의 프로그래밍&lt;/strong&gt;(2024)도 &quot;VO는 이러한 불변성이라는 특징을 갖고 있는 객체를 말합니다&quot;(43쪽)라고 설명하고, 불변성·동등성·자가 검증이라는 세 특성을 만족하는 객체를 VO로 정의합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JEP 401의 value object처럼 필드가 암묵적으로 final이 되어 얕은 불변성이 언어 차원에서 강제되는 개념도 있습니다. 필드에 다른 인스턴스를 대입할 수는 없지만, 필드가 참조하는 객체의 내부 상태까지 불변이 되지는 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 불변성을 VO의 정의 요건이라기보다는 바람직한 설계 규범으로 봅니다. c2 wiki에서 Fowler도 새로 설계하는 객체는 불변으로 만들라고 썼습니다. 새로 만드는 Value Object는 어느 관점을 따르든 불변으로 설계하므로, 실무에서 should와 must의 차이가 드러나는 경우는 많지 않습니다. 그래도 이 구분이 무의미하지는 않습니다. &lt;code&gt;java.util.Date&lt;/code&gt;처럼 가변으로 만들어진 값 성격의 객체를 만났을 때, 불변성을 정의에 포함하면 그 객체는 VO가 아닌 무언가가 되고, 지침으로 보면 불변 지침을 지키지 못한 VO가 됩니다. c2 wiki의 Fowler 문장은 후자의 관점에서 나온 실용적인 조언입니다. 불변 지침을 지키지 못한 VO를 이미 마주쳤다면, 그것을 VO가 아니라고 배제하는 대신 최소한 불변인 것처럼 다루라는 뜻입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;술을 마시고 운전하지 않는다&amp;#8217;를 운전자의 정의에 포함할 것인지, 운전자가 지켜야 할 규범으로 볼 것인지의 차이와 비슷합니다. 어느 쪽이든 술을 마시고 운전하면 안 된다는 결론은 같지만, 규범까지 정의에 포함하면 현실에 존재하는 위반 사례를 부를 이름이 사라져서 그런 사례를 논의하기가 어려워집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dtodata_transfer_object의_정의&quot;&gt;DTO(Data Transfer Object)의 정의&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Fowler의 원래 정의에서 DTO는 원격 호출의 비용을 줄이기 위한 객체입니다. &lt;a href=&quot;https://martinfowler.com/eaaCatalog/dataTransferObject.html&quot;&gt;Martin Fowler의 &lt;strong&gt;Patterns of Enterprise Application Architecture&lt;/strong&gt; 카탈로그&lt;/a&gt;는 DTO를 다음과 같이 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;An object that carries data between processes in order to reduce the number of method calls.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드 호출 횟수를 줄이기 위해 프로세스 사이에서 데이터를 나르는 객체.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;Patterns of Enterprise Application Architecture 카탈로그: Data Transfer Object&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;책의 401쪽에서도 같은 정의를 볼 수 있습니다. 원격 호출은 매번 네트워크 왕복과 직렬화 비용이 들므로 한 번의 호출로 필요한 데이터를 모두 전달하려는 의도에서 나온 패턴입니다. 예를 들어 고객의 이름, 주소, 주문 목록을 getter 원격 호출 세 번으로 가져오는 대신, 세 값을 담은 DTO 하나를 한 번의 호출로 받습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HTTP API의 요청과 응답처럼 네트워크를 건너는 데이터를 담는 객체는 프로세스 경계를 넘고 직렬화된다는 점에서 이 정의와 통합니다. 다만 모든 HTTP API가 원격 호출 횟수를 줄이기 위해 설계되는 것은 아니므로, 이를 DTO라고 부르는 용법은 원래 정의를 현대적인 API 경계에 확장한 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실무에서는 여기에서 더 나아가 이 정의를 폭넓게 해석해서, 원격 호출과 무관하게 같은 프로세스 안에서 계층의 경계를 넘어 데이터를 운반하는 객체까지 DTO라고 부르는 관례가 생겼습니다. DB 조회 결과를 담는 객체, 서비스 계층에서 뷰 렌더링 계층으로 전달하는 객체, JPA 엔티티를 서비스 계층 바깥에 직접 노출하지 않도록 변환한 응답 전용 객체가 그런 예입니다. 이런 용법은 원격 호출 횟수를 줄인다는 원래 정의와 거리가 멀어서, DTO라고 부르기 어렵다는 주장도 나올 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이름을 둘러싼 논쟁과는 별개로, 로컬 맥락에서 그런 객체를 두는 것이 설계상 바람직한지도 따져 볼 수 있습니다. Fowler는 &lt;a href=&quot;https://martinfowler.com/bliki/LocalDTO.html&quot;&gt;LocalDTO&lt;/a&gt;에서 이름이 아니라 이 쓰임새를 문제 삼습니다. DTO 패턴의 존재 이유가 원격 호출 비용을 줄이는 것이므로, 원격 호출이 없는 로컬 맥락에서는 그 이유가 사라진다는 것이 출발점입니다. 그래서 로컬 맥락에서는 DTO가 필요하지 않을 뿐 아니라 오히려 해롭다고 말합니다. 한 번의 호출로 많은 데이터를 주고받는 coarse-grained API는 사용하기 불편하고, 도메인 계층이나 데이터 소스 계층에서 DTO로 데이터를 옮기는 작업이 모두 추가 비용이라는 이유입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Not just do you not need them in a local context, they are actually harmful both because a coarse-grained API is more difficult to use and because you have to do all the work moving data from your domain or data source layer into the DTOs.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로컬 맥락에서는 DTO가 필요 없을 뿐 아니라 실제로 해롭다. Coarse-grained API는 사용하기 더 어렵고, 도메인 계층이나 데이터 소스 계층의 데이터를 DTO로 옮기는 작업을 모두 해야 하기 때문이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;LocalDTO&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서비스 계층의 클라이언트가 도메인 모델에 의존하지 않도록 서비스 계층 API에 DTO를 두자는 주장에 대해서도, 편리할 수는 있지만 그 모든 데이터 매핑 비용을 들일 가치는 없다고 답합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 같은 글에서 프레젠테이션 계층의 모델과 도메인 모델의 차이가 클 때는 로컬에서도 DTO와 비슷한 객체가 유용하다고 인정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;One case where it is useful to use something like a DTO is when you have a significant mismatch between the model in your presentation layer and the underlying domain model.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DTO 같은 것을 쓰는 것이 유용한 경우 하나는 프레젠테이션 계층의 모델과 그 아래의 도메인 모델 사이에 큰 불일치가 있을 때다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Martin Fowler&lt;br&gt;
&lt;cite&gt;LocalDTO&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 경우에는 어차피 두 모델 사이의 매핑이 필요하므로 DTO가 추가 비용이 아니기 때문입니다. 같은 글의 뒷부분에는 멀티스레드 애플리케이션에서 격리된 영역 사이에 메시지로 데이터를 주고받을 때 DTO를 쓰는 용도도 덧붙여 있습니다. 정리하면 Fowler의 비판은 도메인 모델과의 분리 자체를 목적으로 DTO를 두는 경우를 향한 것이고, 모델의 차이나 격리된 영역 사이의 메시지 전달처럼 실제 필요가 있는 경우까지 부정하지는 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이름 논쟁과 쓰임새 논쟁은 서로 다른 질문이지만 같은 전제에서 출발합니다. 로컬 맥락에는 원격 호출이 없다는 사실이, 한쪽에서는 DTO라는 이름이 원래 정의와 맞지 않는다는 근거가 되고, 다른 쪽에서는 DTO를 둘 이유가 약해진다는 근거가 됩니다. 뒤에서 제안하는 역할별 접미어 분리는 이 중 이름 논쟁에 대한 답입니다. 그런 객체를 둘지는 쓰임새 논쟁에서 따로 판단할 문제이고, 두기로 결정했다면 &lt;code&gt;Dto&lt;/code&gt;보다 역할이 드러나는 이름을 붙이자는 제안입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;결국 VO와 DTO를 구분하는 기준은 객체의 특성과 역할입니다. 값 개념을 표현하며 동등성을 값으로 판단하는 것은 VO의 성질이고, 경계를 넘어 데이터를 운반하는 것은 DTO의 역할입니다. 이 둘은 서로 배타적인 분류가 아닙니다. 값 동등성이 있는 불변 DTO는 VO이기도 합니다. 반대로 모든 DTO에 값 동등성이 필요한 것은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;core_j2ee_patterns_초판의_vo와_개정판의_to&quot;&gt;Core J2EE Patterns 초판의 VO와 개정판의 TO&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java/J2EE(현 Jakarta EE) 진영에서 두 용어의 혼용을 확산시킨 대표적인 초기 출처는 Deepak Alur 등이 쓴 &lt;strong&gt;Core J2EE Patterns: Best Practices and Design Strategies&lt;/strong&gt;입니다. 2001년에 나온 초판에서는 티어(tier) 사이에서 데이터를 전송하는 객체를 Value Object라는 이름의 패턴으로 정의했습니다. 2003년의 2판에서는 같은 패턴을 TO(Transfer Object)로 개칭했습니다. 값 동등성 객체로서의 Value Object와의 혼동을 피하려는 개칭으로 보입니다. Martin Fowler는 같은 역할의 패턴을 DTO라고 부릅니다(&lt;strong&gt;Patterns of Enterprise Application Architecture&lt;/strong&gt; 401쪽). 정리하면 다음 세 가지는 같은 전송 패턴을 가리킵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Core J2EE Patterns 초판의 VO = 2판의 TO = Martin Fowler의 DTO&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/transfer-object.html&quot;&gt;Oracle의 Transfer Object 문서&lt;/a&gt;는 개칭된 이름인 Transfer Object로 이 패턴을 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다른 여러 책도 VO와 DTO가 같은 의미이거나 매우 가까운 개념이라고 썼습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Rod Johnson, &lt;strong&gt;Expert One-on-One J2EE Design and Development&lt;/strong&gt;, Wrox, 2002, 265쪽&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Value objects are sometimes referred to as Data Transfer Objects (DTOs).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Value object는 DTO(Data Transfer Object)라고 불리기도 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rod Johnson·Juergen Hoeller, &lt;strong&gt;Expert One-on-One J2EE Development without EJB&lt;/strong&gt;, Wrox, 2004, 27쪽&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Transfer objects, often referred to as Data Transfer Objects (DTOs) or Value Objects.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;흔히 DTO(Data Transfer Object)나 Value Object라고도 부르는 Transfer object.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Murat Yener·Alex Theedom, &lt;strong&gt;Professional Java EE Design Patterns&lt;/strong&gt;, Wrox, 2014, 12장&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The DTO is also referred to as the Value Object&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DTO는 Value Object라고도 불린다&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Derek C. Ashmore, &lt;strong&gt;The Java EE Architect&amp;#8217;s Handbook, Second Edition&lt;/strong&gt;, DVT Press, 2014, 5장&lt;/p&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;My definition of &apos;value object&apos; is very close to a Data Transfer Object (DTO)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;내가 정의하는 &apos;value object&amp;#8217;는 DTO(Data Transfer Object)에 매우 가깝다&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책들이 모두 Core J2EE Patterns 초판의 직접적인 영향을 받았다고 단정할 수는 없습니다. 다만 등장 시기와 여러 문헌에서 반복되는 표현을 고려하면, 초판의 용법이 이후 Java/J2EE 문헌의 표현에도 어느 정도 영향을 미치지 않았을까 추정합니다. 2판에서 이름을 바꾼 지 20년이 넘은 지금까지도 그 관행은 남아 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;데이터_운반_객체를_vo라고_부를_때의_비용&quot;&gt;데이터 운반 객체를 VO라고 부를 때의 비용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 팀 안에서 통용되는 이름을 굳이 바꿔야 하는지 의문이 들 수도 있습니다. 그러나 데이터 운반 객체를 VO라고 부르는 관행에는 실질적인 비용이 있습니다. 값 동등성 객체로서의 Value Object가 DDD, ORM, Java 언어·JVM 기능이라는 세 맥락에서 계속 등장하기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DDD(Domain-Driven Design): VALUE OBJECT는 ENTITY와 함께 도메인 모델을 구성하는 핵심 요소입니다. AGGREGATE는 하나의 ENTITY를 루트로 삼는 데이터 변경의 일관성 경계이며, 내부에 다른 ENTITY와 VALUE OBJECT를 포함할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ORM: Hibernate와 JPA의 &lt;code&gt;@Embeddable&lt;/code&gt;은 자체 영속 식별성 없이 그것을 소유하는 엔티티에 종속되는 객체를 매핑합니다. DDD의 VALUE OBJECT를 매핑할 때 흔히 사용하는 구조이지만, JPA가 값 동등성이나 불변성을 강제하지는 않으므로 모든 &lt;code&gt;@Embeddable&lt;/code&gt;이 곧 DDD VALUE OBJECT인 것은 아닙니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java 언어·JVM 기능: JEP 401을 비롯한 Project Valhalla의 문서들은 value object라는 용어를 불변이고 객체 식별성이 없으며 필드 값으로 구분되는 객체라는 의미로 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;getter/setter만 가진 데이터 홀더를 VO라고 이해하고 있다면 이런 자료를 읽을 때 용어가 어긋나서 혼란이 생깁니다. 예를 들어 VO를 &lt;code&gt;@Embeddable&lt;/code&gt;로 매핑하라는 설명을 읽으면서 setter가 가득 정의된 요청 파라미터 객체를 떠올리게 됩니다. 원격 프로세스 경계를 넘는 데이터 운반 객체를 DTO라고 부르면 Fowler의 정의와도, Core J2EE Patterns 2판 이후의 개칭된 이름과도 자연스럽게 연결됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 DTO 정의 절에서 본 것처럼 근래의 관례로는 계층 경계를 넘는 데이터 홀더라는 넓은 의미로 DTO를 쓰기도 합니다. 그러나 DTO 접미어를 모든 운반 객체에 붙이면 단점이 있습니다. HTTP 요청 파라미터를 담는 객체, 통계 쿼리의 결과를 담는 객체까지 모두 DTO라고 부르면 이름만으로는 역할이 드러나지 않습니다. 예를 들어 &lt;code&gt;IssueDto&lt;/code&gt;라는 이름만 보고는 요청 본문인지, 조회 응답인지, 통계 쿼리의 결과인지 알 수 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 객체의 역할에 따라 접미어를 나누는 방법을 권합니다. 이렇게 하면 이름만으로 역할이 드러나고, 원격 호출과 관련되지 않은 계층에서 쓰이는 객체를 DTO라고 부를 때 엄밀한 정의에서 벗어난다는 논쟁도 피할 수 있습니다. 예를 들면 다음과 같은 이름입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;역할&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;클래스 이름 예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;이슈 조회 JSON 응답&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IssueResponse&lt;/code&gt;, &lt;code&gt;IssueDetailDto&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;이슈 생성 JSON 요청&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IssueCreationRequest&lt;/code&gt;, &lt;code&gt;IssueCreationCommand&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;이슈 조회 조건&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IssueQuery&lt;/code&gt;, &lt;code&gt;IssueCriteria&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;이슈 DB 통계 조회 결과&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IssueStatsRow&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;value_object&quot;&gt;Value Object&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/bliki/ValueObject.html&quot;&gt;Martin Fowler: Value Object&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/20110104080034/http://martinfowler.com/bliki/ValueObject.html&quot;&gt;Martin Fowler: Value Object (2016년 개정 전 버전, Wayback Machine)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.c2.com/?ValueObjectsShouldBeImmutable&quot;&gt;C2 Wiki: Value Objects Should Be Immutable&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Value_object&quot;&gt;Wikipedia: Value object&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects&quot;&gt;Microsoft: 값 개체 구현&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Martin Fowler, &lt;a href=&quot;https://www.informit.com/store/patterns-of-enterprise-application-architecture-9780321127426&quot;&gt;&lt;strong&gt;Patterns of Enterprise Application Architecture&lt;/strong&gt;&lt;/a&gt;, Addison-Wesley, 2002, 486쪽&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eric Evans, &lt;a href=&quot;https://www.informit.com/store/domain-driven-design-tackling-complexity-in-the-heart-9780321125217&quot;&gt;&lt;strong&gt;Domain-Driven Design&lt;/strong&gt;&lt;/a&gt;, Addison-Wesley, 2003&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf&quot;&gt;Eric Evans: Domain-Driven Design Reference (2015), Value Objects&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;김우근, &lt;a href=&quot;https://wikibook.co.kr/pragmatic-programming/&quot;&gt;&lt;strong&gt;자바/스프링 개발자를 위한 실용주의 프로그래밍&lt;/strong&gt;&lt;/a&gt;, 위키북스, 2024, 43쪽&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Joshua Bloch, &lt;a href=&quot;https://www.informit.com/store/effective-java-9780134685991&quot;&gt;&lt;strong&gt;Effective Java&lt;/strong&gt;&lt;/a&gt; 3판, Addison-Wesley, 2018, Item 10·11·17&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vaughn Vernon, &lt;a href=&quot;https://www.informit.com/store/implementing-domain-driven-design-9780133039894&quot;&gt;&lt;strong&gt;Implementing Domain-Driven Design&lt;/strong&gt;&lt;/a&gt;, Addison-Wesley, 2013, 6장&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://c2.com/ppr/checks.html&quot;&gt;Ward Cunningham: The CHECKS Pattern Language of Information Integrity - Whole Value&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/395&quot;&gt;JEP 395: Records&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/401&quot;&gt;JEP 401: Value Objects (Preview)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/169&quot;&gt;JEP 169: Larval State for Value Objects&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;transfer_object_dto&quot;&gt;Transfer Object / DTO&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/eaaCatalog/dataTransferObject.html&quot;&gt;Martin Fowler: Data Transfer Object&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/bliki/LocalDTO.html&quot;&gt;Martin Fowler: LocalDTO&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Martin Fowler, &lt;a href=&quot;https://www.informit.com/store/patterns-of-enterprise-application-architecture-9780321127426&quot;&gt;&lt;strong&gt;Patterns of Enterprise Application Architecture&lt;/strong&gt;&lt;/a&gt;, Addison-Wesley, 2002, 401쪽&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deepak Alur·John Crupi·Dan Malks, &lt;a href=&quot;https://www.amazon.com/Core-J2EE-Patterns-Practices-Strategies/dp/0130648841&quot;&gt;&lt;strong&gt;Core J2EE Patterns: Best Practices and Design Strategies&lt;/strong&gt;&lt;/a&gt; 초판, Prentice Hall, 2001&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rod Johnson, &lt;a href=&quot;https://www.wiley.com/en-us/Expert+One+on+One+J2EE+Design+and+Development-p-9780764543852&quot;&gt;&lt;strong&gt;Expert One-on-One J2EE Design and Development&lt;/strong&gt;&lt;/a&gt;, Wrox, 2002, 265쪽&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rod Johnson·Juergen Hoeller, &lt;a href=&quot;https://www.wiley.com/en-us/Expert+One+on+One+J2EE+Development+without+EJB-p-9780764573903&quot;&gt;&lt;strong&gt;Expert One-on-One J2EE Development without EJB&lt;/strong&gt;&lt;/a&gt;, Wrox, 2004, 27쪽&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Murat Yener·Alex Theedom, &lt;a href=&quot;https://www.wiley.com/en-us/Professional+Java+EE+Design+Patterns-p-9781118843413&quot;&gt;&lt;strong&gt;Professional Java EE Design Patterns&lt;/strong&gt;&lt;/a&gt;, Wrox, 2014, 12장&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Derek C. Ashmore, &lt;a href=&quot;https://www.amazon.com/Java-Architects-Handbook-Second-applications/dp/0972954880&quot;&gt;&lt;strong&gt;The Java EE Architect&amp;#8217;s Handbook, Second Edition&lt;/strong&gt;&lt;/a&gt;, DVT Press, 2014, 5장&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/transfer-object.html&quot;&gt;Oracle: Core J2EE Patterns - Transfer Object&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.adam-bien.com/roller/abien/entry/value_object_vs_data_transfer&quot;&gt;Adam Bien: Value Object vs. Data Transfer Object (VO vs. DTO)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;관련_발표_자료&quot;&gt;관련 발표 자료&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/presentations/blob/main/20200507-entity-design/slides.md&quot;&gt;엔티티 클래스 설계와 퍼시스턴스 프레임워크 (2020년 발표)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>GitHub Actions 워크플로 튜닝: 저장소 다섯 곳의 적용 사례</title>
      <link>https://blog.benelog.net//github-actions-tuning-cases.html</link>
      <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">github-actions-tuning-cases.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#spider_silk_측정으로_찾은_병목&quot;&gt;1. spider-silk: 측정으로 찾은 병목&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#flashcard_트리거_필터와_중복_실행_취소&quot;&gt;2. flashcard: 트리거 필터와 중복 실행 취소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#spring_jdbc_book_경로_필터와_캐시_손익_실험&quot;&gt;3. spring-jdbc-book: 경로 필터와 캐시 손익 실험&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#pdf_refinery_매트릭스_병렬화와_캐시_후보&quot;&gt;4. pdf-refinery: 매트릭스 병렬화와 캐시 후보&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#til_배포_워크플로의_concurrency_설정&quot;&gt;5. til: 배포 워크플로의 concurrency 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;6. 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;7. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;github-actions-tuning.html&quot;&gt;앞 글&lt;/a&gt;에서 GitHub Actions의 실행 파이프라인 구조와, 측정 → 실행 횟수 줄이기 → 캐시 → 병렬화 → 러너 선택 순서의 튜닝 기법을 정리했습니다.
이 글은 그 기법을 제 개인 저장소 다섯 곳에 적용하며 측정한 기록입니다.
기법의 원리와 주의점은 앞 글에 있으므로, 여기서는 각 저장소에서 무엇을 적용했고 결과가 어땠는지에 집중합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대상은 &lt;a href=&quot;https://github.com/benelog/flashcard&quot;&gt;benelog/flashcard&lt;/a&gt;, &lt;a href=&quot;https://github.com/benelog/spider-silk&quot;&gt;benelog/spider-silk&lt;/a&gt;, &lt;a href=&quot;https://github.com/benelog/pdf-refinery&quot;&gt;benelog/pdf-refinery&lt;/a&gt;, &lt;a href=&quot;https://github.com/benelog/spring-jdbc-book&quot;&gt;benelog/spring-jdbc-book&lt;/a&gt;, &lt;a href=&quot;https://github.com/benelog/til&quot;&gt;benelog/til&lt;/a&gt;입니다.
모두 공개 저장소라 4 vCPU / 16GB 사양의 표준 Linux 러너에서 실행됐습니다.
측정값은 2026년 8월 28일에 확인한 실제 실행 기록에서 가져왔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spider_silk_측정으로_찾은_병목&quot;&gt;1. spider-silk: 측정으로 찾은 병목&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;spider-silk의 Publish 워크플로에 앞 글의 &lt;code&gt;gh run view&lt;/code&gt; 명령을 돌린 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;JOB publish: 354s
  Set up job: 3s
  Run actions/checkout@v7: 1s
  Run actions/setup-java@v4: 0s
  Run gradle/actions/setup-gradle@v6: 9s
  Run ./gradlew build: 58s
  Run ./gradlew publishAllPublicationsToGitHubPackagesRepository: 276s
  Post Run gradle/actions/setup-gradle@v6: 3s&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전체 354초 중 276초를 GitHub Packages에 아티팩트를 올리는 데 씁니다.
빌드는 58초입니다.
Gradle 빌드 캐시를 아무리 잘 맞춰도 이 워크플로는 6분에서 5분으로밖에 줄지 않습니다.
측정하지 않고 &quot;Gradle 빌드가 느리다&quot;고 짐작해서 캐시 설정부터 손댔다면, 효과가 거의 없는 곳에 시간을 쓰게 됐을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;flashcard_트리거_필터와_중복_실행_취소&quot;&gt;2. flashcard: 트리거 필터와 중복 실행 취소&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;flashcard의 CI는 Go 코드를 검사하는 워크플로라, 원고 디렉터리만 고쳤을 때는 실행하지 않도록 &lt;code&gt;paths-ignore&lt;/code&gt;를 걸었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;flashcard/.github/workflows/ci.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;on:
  push:
    branches: [main, release]
    paths-ignore:
      - &apos;book/**&apos;
      - &apos;book-template/**&apos;
  pull_request:
    paths-ignore:
      - &apos;book/**&apos;
      - &apos;book-template/**&apos;
  workflow_dispatch:&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 브랜치에 커밋을 연달아 밀 때의 중복 실행은 &lt;code&gt;concurrency&lt;/code&gt;로 취소합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;flashcard/.github/workflows/ci.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대기 시간도 확인했습니다.
실행 생성이 &lt;code&gt;14:04:15Z&lt;/code&gt;, 잡 시작이 &lt;code&gt;14:04:18Z&lt;/code&gt;로 대기가 3초였습니다.
개인 저장소라 러너 배정 대기는 문제가 아니었고, 튜닝 대상은 실행 시간 쪽임을 확인했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;잡 분리의 준비 비용도 이 저장소에서 확인할 수 있습니다.
&lt;code&gt;Set up job&lt;/code&gt; 1초, 체크아웃 2초, &lt;code&gt;actions/setup-go&lt;/code&gt; 7초로 실제 작업 전에 10초를 씁니다.
잡을 나누면 이 10초를 잡마다 다시 내므로, 이 정도 규모의 워크플로에서는 잡 분리가 이득이 되기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spring_jdbc_book_경로_필터와_캐시_손익_실험&quot;&gt;3. spring-jdbc-book: 경로 필터와 캐시 손익 실험&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;spring-jdbc-book은 예제 코드가 바뀔 때만 테스트를 돌리도록 &lt;code&gt;paths&lt;/code&gt;로 대상을 지정했습니다.
워크플로 파일 자신을 목록에 넣어 둔 점이 중요합니다. 이렇게 해야 워크플로를 고쳤을 때 그 변경이 검증됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;spring-jdbc-book/.github/workflows/test-examples.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;on:
  push:
    branches: [main]
    paths:
      - &apos;examples/**&apos;
      - &apos;.github/workflows/test-examples.yml&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;캐시가_잡_시간을_줄이지_못한_실험&quot;&gt;3.1. 캐시가 잡 시간을 줄이지 못한 실험&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 저장소의 테스트 워크플로를 4분 간격으로 두 번 실행해 캐시의 손익을 쟀습니다.
첫 실행에는 복원할 캐시가 없었고, 두 번째 실행은 첫 실행이 저장해 둔 캐시를 복원했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;단계&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;1회차&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;2회차&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Set up Gradle (캐시 복원)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;3초&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;13초&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Run tests&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;113초&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;99초&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Post Set up Gradle (캐시 저장)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;39초&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;40초&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;잡 전체&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;161초&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;159초&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시 덕분에 테스트 실행은 113초에서 99초로 14초 줄었습니다.
그런데 복원에 10초가 더 들고 저장 시간은 그대로라, 잡 전체로는 161초에서 159초가 되어 차이가 없었습니다.
이 저장소의 예제는 의존성이 적고 Testcontainers로 PostgreSQL을 띄우는 시간이 테스트 시간의 대부분이라, 캐시로 줄일 수 있는 몫이 애초에 작았습니다.
두 번만 비교한 값이라 테스트 시간 14초 차이에는 러너 성능 편차도 섞여 있을 수 있습니다. 그래도 캐시 저장에 매번 40초 가까이 든다는 점은 두 실행에서 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;실패했을_때만_올리는_테스트_리포트&quot;&gt;3.2. 실패했을 때만 올리는 테스트 리포트&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아티팩트는 필요할 때만 올립니다.
이 워크플로는 테스트 리포트를 실패했을 때만 업로드합니다.
성공한 실행에서는 이 단계가 0초로 끝납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;spring-jdbc-book/.github/workflows/test-examples.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;      - name: Upload test reports on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: examples/*/build/reports/tests/test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;pdf_refinery_매트릭스_병렬화와_캐시_후보&quot;&gt;4. pdf-refinery: 매트릭스 병렬화와 캐시 후보&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;pdf-refinery는 Python 세 버전을 매트릭스로 돌립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;pdf-refinery/.github/workflows/test.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [&quot;3.10&quot;, &quot;3.11&quot;, &quot;3.12&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실행 기록을 보면 세 잡이 각각 305초, 240초, 308초 걸렸습니다.
러너 사용 시간의 합은 853초지만, 세 잡이 동시에 시작해서 실제로 기다린 시간은 가장 느린 308초입니다.
버전별 검증이라는 목적을 러너 준비 비용(잡당 10초 안팎)만 더 내고 체감 시간 증가 없이 달성한 셈입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시는 아직 붙이지 않았습니다.
매 실행마다 &lt;code&gt;pip install -e &apos;.[dev]&apos;&lt;/code&gt;에 41~50초를 쓰는데, 이 시간은 거의 전부 다운로드라서 캐시를 붙이면 줄어들 가능성이 높습니다.
spring-jdbc-book과 달리 이쪽은 캐시가 이득일 후보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;til_배포_워크플로의_concurrency_설정&quot;&gt;5. til: 배포 워크플로의 concurrency 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;til의 Cloud Run 배포 워크플로는 중간에 취소되면 어중간한 상태로 남을 수 있습니다.
그래서 &lt;code&gt;cancel-in-progress&lt;/code&gt;를 켜지 않고, 그룹만 묶어 한 번에 하나만 돌게 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;til/.github/workflows/deploy.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;# 연달아 푸시해도 배포가 서로 앞지르지 않게 한 번에 하나만 돌린다.
concurrency:
  group: deploy-cloud-run
  cancel-in-progress: false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 설정에서 진행 중인 실행은 끝까지 돌고 새 실행은 대기합니다.
다만 기본값으로는 한 그룹에서 대기할 수 있는 실행이 하나뿐이라, 커밋 세 개를 연달아 밀면 첫 번째는 배포되고 두 번째는 취소되며 세 번째만 배포됩니다.
중간 실행까지 모두 돌려야 하는 워크플로라면 앞 글에서 다룬 &lt;code&gt;queue: max&lt;/code&gt;를 고려해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;6. 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다섯 저장소에 적용한 내용과 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 50.0001%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;저장소&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;적용·측정한 것&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;spider-silk&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;단계별 소요 시간 측정&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;병목은 빌드(58초)가 아니라 GitHub Packages 업로드(276초). 캐시 튜닝으로는 줄일 수 없는 구조&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;flashcard&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;paths-ignore&lt;/code&gt;, &lt;code&gt;concurrency&lt;/code&gt;, 대기 시간 측정&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;원고만 고친 커밋은 실행 생략. 대기는 3초라 문제가 아님&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;spring-jdbc-book&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;paths&lt;/code&gt; 필터, 캐시 실험, 조건부 아티팩트&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시를 붙여도 잡 전체는 161초 → 159초로 효과 없음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;pdf-refinery&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;매트릭스 병렬화&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;러너 시간 합 853초를 체감 308초로. &lt;code&gt;pip install&lt;/code&gt; 41~50초는 캐시 후보&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;til&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;배포 워크플로의 &lt;code&gt;concurrency&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cancel-in-progress&lt;/code&gt;를 꺼서 배포 중단 방지&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다섯 저장소에서 공통으로 확인한 것은 측정이 먼저라는 점입니다.
spider-silk처럼 병목이 짐작과 다른 곳에 있기도 하고, spring-jdbc-book처럼 정석으로 알려진 캐시가 측정해 보면 이득이 없기도 합니다.
기법을 적용하기 전과 후의 잡 전체 시간을 비교하는 습관이 어떤 설정보다 효과가 컸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;7. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;이 블로그&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;github-actions-tuning.html&quot;&gt;GitHub Actions의 실행 구조와 워크플로 튜닝 기법&lt;/a&gt; — 이 글에서 적용한 기법들의 원리와 주의점&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;GitHub 공식 문서&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cli.github.com/manual/gh_run_view&quot;&gt;gh run view - GitHub CLI manual&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/concepts/metrics&quot;&gt;About GitHub Actions metrics&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching&quot;&gt;Dependency caching&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>GitHub Actions의 실행 구조와 워크플로 튜닝 기법</title>
      <link>https://blog.benelog.net//github-actions-tuning.html</link>
      <pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">github-actions-tuning.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#이벤트에서_러너까지의_실행_파이프라인과_장애&quot;&gt;1. 이벤트에서 러너까지의 실행 파이프라인과 장애&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#단계별_소요_시간_측정&quot;&gt;2. 단계별 소요 시간 측정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#실행_횟수_줄이기&quot;&gt;3. 실행 횟수 줄이기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#의존성_캐시와_그_비용&quot;&gt;4. 의존성 캐시와 그 비용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#잡_분리와_매트릭스의_손익&quot;&gt;5. 잡 분리와 매트릭스의 손익&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#러너_사양과_종류_선택&quot;&gt;6. 러너 사양과 종류 선택&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#체크아웃과_아티팩트_다듬기&quot;&gt;7. 체크아웃과 아티팩트 다듬기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#github_enterprise_server에서_달라지는_손익_계산&quot;&gt;8. GitHub Enterprise Server에서 달라지는 손익 계산&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;9. 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;10. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GitHub Actions 워크플로가 느려지면 푸시한 뒤 결과를 확인하기까지 기다리는 시간이 길어지고, 리뷰와 배포도 함께 밀립니다.
그런데 워크플로 파일만 보고 어느 설정을 고쳐야 할지 정하기는 어렵습니다.
느린 원인이 빌드일 수도, 의존성 내려받기일 수도, 러너를 배정받기까지의 대기일 수도 있기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글에서는 먼저 워크플로가 실행되기까지 GitHub 내부에서 거치는 파이프라인 구조와 최근 장애 이력을 살펴봅니다.
그 다음에 실행 시간을 줄이는 기법을 측정 → 실행 횟수 줄이기 → 캐시 → 병렬화 → 러너 선택 순서로 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GitHub의 사양·요금·한도는 2026년 8월 28일에 확인한 값입니다. 바뀔 수 있으므로 글 끝의 공식 문서를 함께 확인하시기를 권합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;이벤트에서_러너까지의_실행_파이프라인과_장애&quot;&gt;1. 이벤트에서 러너까지의 실행 파이프라인과 장애&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구체적인 튜닝 기법을 보기 전에, 푸시 한 번이 러너 위에서 도는 잡이 되기까지 GitHub 내부에서 거치는 경로를 정리합니다.
튜닝하려면 시간이 어디에서 소모되는지 알아야 하는데, GitHub이 Actions의 내부 구조를 직접 공개하지는 않습니다.
대신 공식 문서와 장애 보고서(availability report)에 흩어져 있는 내부 서비스에 대한 언급으로 구조를 재구성할 수 있고, 장애 사례는 그 구조의 어느 단계가 실제로 막히는지도 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;워크플로를_트리거하는_이벤트의_종류&quot;&gt;1.1. 워크플로를 트리거하는 이벤트의 종류&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;워크플로는 &lt;code&gt;on&lt;/code&gt;에 선언한 이벤트가 발생할 때 실행됩니다.
이벤트는 성격에 따라 네 갈래로 나뉩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 22.2222%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 44.4445%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;분류&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;이벤트&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;특성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;webhook 이벤트&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull_request&lt;/code&gt;, &lt;code&gt;issues&lt;/code&gt; 등&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;저장소 활동이 만드는 이벤트입니다. &lt;code&gt;issues&lt;/code&gt;처럼 특정 브랜치에 묶이지 않는 이벤트는 기본 브랜치의 워크플로 파일만 트리거합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;스케줄&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;schedule&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;cron 문법으로 지정하고 기본 브랜치에서만 실행됩니다. 부하가 높은 시간대에는 실행이 지연될 수 있습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;수동·외부 트리거&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt;, &lt;code&gt;repository_dispatch&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt;는 사용자가 UI·CLI·API로 직접 시작합니다. &lt;code&gt;repository_dispatch&lt;/code&gt;는 외부 시스템이 REST API를 호출해 트리거합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;워크플로 간 트리거&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;workflow_run&lt;/code&gt;, &lt;code&gt;workflow_call&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;workflow_run&lt;/code&gt;은 다른 워크플로의 시작·완료에 반응해 별도 실행을 만듭니다. &lt;code&gt;workflow_call&lt;/code&gt;은 재사용 워크플로를 호출하는 메커니즘이라, 호출한 쪽 실행 안에서 돌고 별도 실행을 만들지 않습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;성능 관점에서는 &lt;code&gt;schedule&lt;/code&gt;의 공식 문서 문구를 알아둘 만합니다.
GitHub은 &lt;code&gt;schedule&lt;/code&gt; 이벤트가 Actions 전체의 부하가 높은 시간대에 지연될 수 있고, 매시 정각이 바로 그런 시간대이며, 부하가 충분히 높으면 큐잉된 잡이 아예 드랍될 수도 있다고 안내합니다.
cron의 분을 0이 아닌 값으로 두면 정각에 몰리는 혼잡을 피할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이벤트 자체가 생성되지 않는 경우도 있습니다.
한 번에 5,000개가 넘는 브랜치를 푸시하면 push 이벤트가 만들어지지 않고, 태그를 한 번에 3개 넘게 푸시하면 태그에 대한 이벤트가 만들어지지 않습니다.
워크플로가 실행되지 않았는데 로그도 없다면, 워크플로 설정보다 앞 단계인 이벤트 생성이 생략되지 않았는지 의심해 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;내부_큐_구조와_graphql_계층&quot;&gt;1.2. 내부 큐 구조와 GraphQL 계층&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버 쪽 내부 구조를 설명하는 문서는 없지만, 공식 문서와 장애 보고서 곳곳에 언급된 내부 서비스와 의존 시스템의 정보를 이으면 큰 흐름을 재구성할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;이벤트 접수&lt;/strong&gt;: push 같은 저장소 활동은 webhook 이벤트가 되어 Kafka 기반의 백그라운드 잡 파이프라인으로 들어갑니다. 2023년 8월 장애 보고서는 이 파이프라인이 막히자 webhook 전달과 Actions 트리거가 함께 지연됐다고 기록합니다. 즉 Actions는 이벤트를 직접 받는 것이 아니라 GitHub의 공용 비동기 처리 계층을 거쳐 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;실행 생성&lt;/strong&gt;: &quot;이벤트를 처리하고 잡을 생성하는 내부 Actions 서비스&quot;가 저장소의 워크플로 파일에서 트리거 조건을 평가해 워크플로 실행(run)과 잡을 만듭니다. 이 표현은 2026년 8월 6일 장애의 공식 원인 분석에 나옵니다. 이 경로는 커밋에 검사 상태를 기록하는 check suite 생성과 GraphQL 계층도 지납니다. 2021년 3월에는 check suite ID가 Int32 범위를 넘어서자 의존하던 GraphQL 라이브러리가 JSON을 읽지 못했고, 그 결과 잡이 큐에 들어가지 못하는 장애가 있었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;잡 큐잉과 배정&lt;/strong&gt;: 만들어진 잡은 큐에 들어가고, 잡 배정을 담당하는 서비스가 &lt;code&gt;runs-on&lt;/code&gt; 레이블·그룹이 맞는 유휴 러너를 찾아 잡을 배정합니다. 잡 오케스트레이션은 Redis 클러스터에 의존합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;러너 수령&lt;/strong&gt;: 러너는 GitHub으로 아웃바운드 HTTPS 연결만 열고, 50초 단위의 long poll로 잡을 기다립니다. 러너 애플리케이션은 Azure Pipelines Agent의 포크이고, 로그와 아티팩트는 Azure Blob Storage로 올라갑니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/github-actions-tuning/actions-pipeline.png&quot; alt=&quot;이벤트 접수부터 러너 수령까지 GitHub Actions 실행 파이프라인 다이어그램&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이와 같이 워크플로 실행은 여러 큐와 서비스를 지나는 비동기 파이프라인입니다.
webhook 처리 → 실행 생성 → 잡 큐 → 러너 배정 중 어느 구간이 막혀도 사용자에게는 똑같이 &quot;워크플로가 시작되지 않는&quot; 증상으로 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 러너가 GraphQL API를 직접 호출하지는 않습니다.
액션 내려받기와 API 호출은 &lt;code&gt;api.github.com&lt;/code&gt; 등 REST 엔드포인트와 &lt;code&gt;*.actions.githubusercontent.com&lt;/code&gt; 계열 도메인을 씁니다.
워크플로 파일을 읽거나 액션을 resolve하는 단계에서 내부적으로 GraphQL을 쓰는지는 공개된 자료가 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;성능을_좌우하는_조건_평가_시점&quot;&gt;1.3. 성능을 좌우하는 조건 평가 시점&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 구조에서 성능에 크게 영향을 주는 요소는 워크플로 파일이나 조건식의 개수가 아니라, 조건이 파이프라인의 어느 단계에서 평가되는가입니다.
조건은 앞쪽에서 평가될수록 이득입니다.
같은 &quot;건너뛰기&quot;라도 평가 시점이 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;on&lt;/code&gt;의 &lt;code&gt;paths&lt;/code&gt;·&lt;code&gt;branches&lt;/code&gt; 필터는 실행 생성 전에 평가됩니다. 필터에 걸리면 실행 자체가 만들어지지 않아 파이프라인에 아무것도 들어가지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jobs.&amp;lt;job_id&amp;gt;.if&lt;/code&gt;는 실행이 만들어진 뒤 잡 수준에서 평가됩니다. 조건이 거짓이면 잡은 skipped가 되어 러너를 쓰지 않지만, 실행은 생성되어 실행 목록에 남습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 튜닝의 핵심은 실행과 잡을 만들지 않도록 조건을 가능한 한 앞 단(&lt;code&gt;on&lt;/code&gt; 필터)으로 옮기는 일입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반대로 개수는 걱정할 요인이 아닙니다.
트리거 평가는 파일 단위로 일어나므로 파일이 많으면 평가할 대상도 늘어나는 셈이지만, 파일 수가 실행 시작을 늦춘다는 공식 자료나 측정은 찾지 못했습니다.
공식 한도도 파일 수가 아니라 파일 하나의 크기(500KB), 트리거 이벤트 빈도(저장소당 10초에 1,500개), 실행 생성 빈도(10초에 500개)에 걸려 있습니다.
조건식도 마찬가지입니다.
표현식 평가는 서버 쪽에서 일어나고, 그 비용이 실행 시간에 잡힐 만큼 크다는 근거는 없습니다.
워크플로 파일을 합치거나 조건식을 줄이는 정리는 관리 편의로는 의미가 있어도, 실행 시간을 줄이는 수단은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 평가 시점의 차이는 필수 상태 검사 문제와도 이어집니다.
&lt;code&gt;on&lt;/code&gt; 필터로 워크플로를 건너뛰면 검사가 Pending으로 남아 병합이 막히지만, 잡 수준 &lt;code&gt;if&lt;/code&gt;로 건너뛴 잡은 상태가 Success로 보고되어 병합을 막지 않습니다.
그래서 필수 검사로 지정한 워크플로에서는 경로 필터 대신 잡 수준에서 변경 파일을 판별해 건너뛰는 방식이 우회책이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;잦은_actions_장애와_파이프라인_구조의_관계&quot;&gt;1.4. 잦은 Actions 장애와 파이프라인 구조의 관계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞 절에서 장애 보고서는 구조를 재구성하는 근거였는데, 이 절에서는 장애 사례 자체를 봅니다.
장애가 파이프라인의 어느 단계를 막는지, 그리고 그때 생기는 지연이 왜 워크플로 튜닝의 범위 밖인지가 여기서 드러납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2025년 말부터 GitHub의 장애는 잦아지고 있습니다.
월별 availability report에 집계된 인시던트 수가 2025년 하반기에는 월 3~5건이었는데 2026년에는 월 6~10건으로 늘었습니다.
이 글을 쓰는 2026년 8월에는 1일부터 27일까지 githubstatus.com에 올라온 인시던트 25건 중 7건이 Actions 관련이었습니다.
증상도 &quot;실행 시작 지연&quot;, &quot;잡이 queued 상태로 고착&quot;처럼 파이프라인 앞 단이 막히는 유형이 반복됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원인을 나눠 보면, Actions 파이프라인 자체의 장애와 파이프라인이 의존하는 공용 서비스·인프라의 장애가 섞여 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;파이프라인 자체의 장애&lt;/strong&gt;: 2026년 8월 6일에는 이벤트를 처리해 잡을 생성하는 내부 서비스에 정기 배포가 나가면서 잠재해 있던 용량 문제가 드러났고, 피크에는 워크플로 실행의 71%가 실패했습니다. 잡 배정 서비스의 버그로 러너가 이미 무효가 된 잡을 배정받아 재시도를 반복했고, GitHub은 복구를 위해 webhook 유입을 평소의 15% 수준으로 줄였습니다. 그 밖에도 잡 오케스트레이션용 Redis 클러스터 문제로 실행의 95%가 5분 안에 시작하지 못한 장애(2026년 3월 5일), 러너 관련 서비스의 SSL 인증서 만료(2026년 7월과 8월), 데이터베이스 primary 포화(2026년 8월 26일)가 이어졌습니다. 2026년 5월 26일에는 자동 계정 심사 시스템이 Actions의 내부 서비스 계정을 오인해 정지시키는 바람에 새로 큐잉된 모든 실행이 실패하기도 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;공용 서비스·인프라 장애의 전파&lt;/strong&gt;: Actions는 webhook, API, 데이터베이스·캐시 계층, Azure 인프라에 바탕을 두고 있습니다. 2025년 10월 29일 Azure Front Door 장애 때는 호스티드 러너 잡이 실패했고, 2026년 6월 8일에는 github.com의 릴리스 다운로드 엔드포인트가 504를 반환하자 그 엔드포인트에 의존하는 워크플로들이 실패했습니다. Actions 자체는 정상이어도 의존 대상이 무너지면 함께 무너지는 구조라서, github.com 전반에 걸친 장애에서는 거의 항상 Actions가 영향 목록에 들어갑니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GitHub도 이 구조를 문제로 인정하고, 2026년 3월에 &quot;Actions와 Git 같은 핵심 시스템이 공유 인프라 문제에 영향받지 않도록 의존성을 격리하겠다&quot;고 밝혔습니다.
커뮤니티에서는 진행 중인 데이터센터 이전 작업을 장애 증가와 연결하는 추측도 있지만, 공식 장애 보고서가 이전 작업을 원인으로 지목한 적은 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;장애나 혼잡으로 생기는 대기 시간은 워크플로 안쪽을 아무리 튜닝해도 줄지 않습니다.
그래서 이 글에서는 먼저 튜닝할 대상을 측정할 수 있는 기법을 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;단계별_소요_시간_측정&quot;&gt;2. 단계별 소요 시간 측정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;워크플로를 고치기 전에 어느 단계가 시간을 쓰는지부터 확인해야 합니다.
GitHub UI의 실행 결과 화면에서 각 단계를 펼치면 소요 시간이 나오지만, 여러 실행을 비교하려면 &lt;code&gt;gh&lt;/code&gt; CLI로 뽑는 편이 빠릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gh run view -R &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; &amp;lt;run-id&amp;gt; --json jobs --jq &apos;
  .jobs[] | &quot;JOB \(.name): \(((.completedAt|fromdateiso8601) - (.startedAt|fromdateiso8601)))s&quot;,
  (.steps[] | &quot;  \(.name): \(((.completedAt|fromdateiso8601) - (.startedAt|fromdateiso8601)))s&quot;)&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;측정하지 않고 &quot;빌드가 느리다&quot;고 짐작해서 캐시 설정부터 손대면, 효과가 거의 없는 곳에 시간을 쓰게 됩니다.
전체 시간의 대부분을 빌드가 아닌 단계(예: 아티팩트나 패키지 업로드)가 차지하는 워크플로도 흔합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;대기_시간_확인&quot;&gt;2.1. 대기 시간 확인&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;잡이 실제로 실행된 시간과 별개로, 러너를 배정받기까지 기다리는 시간이 따로 있습니다.
워크플로 실행이 생성된 시각(&lt;code&gt;createdAt&lt;/code&gt;)과 잡이 시작된 시각(&lt;code&gt;startedAt&lt;/code&gt;)의 차이가 대기 시간입니다.
앞에서 본 파이프라인으로 말하면 잡 큐잉부터 러너 배정까지의 구간입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gh run view -R &amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt; &amp;lt;run-id&amp;gt; --json createdAt,jobs \
  --jq &apos;&quot;created: \(.createdAt)&quot;, (.jobs[] | &quot;started: \(.startedAt)&quot;)&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개인 저장소에서는 대기가 거의 없지만, 동시 실행 한도에 걸리는 조직 저장소에서는 대기가 실행 시간보다 길어지기도 합니다.
그런 상황이면 워크플로 내부를 아무리 다듬어도 체감 시간이 줄지 않습니다.
저장소나 조직의 Insights 탭에 있는 Actions Performance Metrics에서 잡별 평균 실행 시간과 평균 대기 시간을 최대 1년 치까지 볼 수 있으니, 여기서 대기가 문제인지 실행이 문제인지를 먼저 구분하는 편이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실행_횟수_줄이기&quot;&gt;3. 실행 횟수 줄이기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 확실하게 시간을 줄이는 방법은 실행 횟수 자체를 줄이는 것입니다.
실행할 필요가 없는 워크플로는 트리거 조건으로 거르고, 결과를 볼 필요가 없어진 실행은 concurrency로 취소합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;트리거_조건으로_불필요한_실행_제거&quot;&gt;3.1. 트리거 조건으로 불필요한 실행 제거&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;on&lt;/code&gt;에 &lt;code&gt;paths&lt;/code&gt;나 &lt;code&gt;paths-ignore&lt;/code&gt;를 걸면 바뀐 파일 경로에 따라 실행을 건너뜁니다.
앞에서 본 대로 이 필터는 실행 생성 전에 평가되므로, 필터에 걸리면 파이프라인에 아무것도 들어가지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;on:
  push:
    branches: [main]
    paths:
      - &apos;examples/**&apos;
      - &apos;.github/workflows/test-examples.yml&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;paths&lt;/code&gt;를 쓸 때는 워크플로 파일 자신을 목록에 넣어 두는 점이 중요합니다.
이렇게 해야 워크플로를 고쳤을 때 그 변경이 검증됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock caution&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caution&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;브랜치 보호 규칙에서 필수 상태 검사(required status check)로 지정한 워크플로에는 경로 필터를 쓰면 안 됩니다.
필터 때문에 워크플로를 건너뛰면 검사 상태가 Pending으로 남고, 풀 리퀘스트는 &quot;Waiting for status to be reported&quot; 상태로 병합이 막힙니다.
GitHub 문서는 필수 검사에 경로·브랜치 필터를 쓰지 말라고 안내하고, 굳이 써야 한다면 같은 이름으로 항상 성공하는 워크플로를 &lt;code&gt;paths-ignore&lt;/code&gt;와 함께 하나 더 두는 우회책을 제시합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;concurrency로_중복_실행_취소&quot;&gt;3.2. concurrency로 중복 실행 취소&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 브랜치에 커밋을 연달아 밀어 넣으면 이전 커밋의 워크플로가 계속 돌고 있는데 새 실행이 또 시작됩니다.
결과를 볼 필요가 없는 실행이 러너를 차지합니다.
&lt;code&gt;concurrency&lt;/code&gt;로 그룹을 묶고 &lt;code&gt;cancel-in-progress&lt;/code&gt;를 켜면 진행 중이던 실행이 취소됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 이 설정을 모든 워크플로에 넣으면 안 됩니다.
배포 워크플로는 중간에 취소되면 어중간한 상태로 남을 수 있습니다.
그런 워크플로는 그룹만 묶고 취소는 끕니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;concurrency:
  group: deploy
  cancel-in-progress: false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;cancel-in-progress: false&lt;/code&gt;로 두면 진행 중인 실행은 끝까지 돌고 새 실행은 대기합니다.
다만 기본값으로는 한 그룹에서 대기할 수 있는 실행이 하나뿐이고, 뒤에 들어온 실행이 먼저 대기하던 실행을 취소합니다.
커밋 세 개를 연달아 밀면 첫 번째는 배포되고 두 번째는 취소되며 세 번째만 배포됩니다.
중간 실행까지 모두 돌려야 한다면 &lt;code&gt;queue: max&lt;/code&gt;를 지정해 최대 100개까지 줄을 세울 수 있습니다.
&lt;code&gt;queue: max&lt;/code&gt;와 &lt;code&gt;cancel-in-progress: true&lt;/code&gt;는 동작이 서로 어긋나므로 함께 쓸 수 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;의존성_캐시와_그_비용&quot;&gt;4. 의존성 캐시와 그 비용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;의존성을 내려받는 시간은 대개 캐시로 줄입니다.
&lt;code&gt;actions/cache&lt;/code&gt;를 직접 쓰기 전에, 언어별 setup 액션에 내장된 캐시 옵션을 먼저 확인하는 편이 간단합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 28.5714%;&quot;&gt;
&lt;col style=&quot;width: 28.5714%;&quot;&gt;
&lt;col style=&quot;width: 42.8572%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;액션&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;캐시 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;actions/setup-go&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cache: true&lt;/code&gt; (기본값)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모듈 캐시와 빌드 캐시&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;actions/setup-node&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cache: npm&lt;/code&gt; 또는 &lt;code&gt;pnpm&lt;/code&gt;, &lt;code&gt;yarn&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;패키지 매니저의 전역 캐시 디렉터리&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;actions/setup-python&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cache: pip&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;pip 캐시 디렉터리&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;actions/setup-java&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cache: gradle&lt;/code&gt; 또는 &lt;code&gt;maven&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;~/.gradle&lt;/code&gt;, &lt;code&gt;~/.m2/repository&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;gradle/actions/setup-gradle&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본 동작&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Gradle 사용자 홈과 설정 캐시&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;캐시의_손익_계산&quot;&gt;4.1. 캐시의 손익 계산&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시는 복원과 저장에도 시간을 씁니다.
복원은 setup 단계에서, 저장은 잡 마지막의 &lt;code&gt;Post&lt;/code&gt; 단계에서 일어나므로, 실행 결과 화면에서 이 두 단계의 시간을 보면 캐시가 얼마를 쓰는지 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시가 이득인지는 &quot;다시 내려받는 시간&quot;과 &quot;복원 + 저장 시간&quot;의 차이로 정해집니다.
의존성 다운로드에 시간을 많이 쓰는 프로젝트는 캐시로 줄어들 가능성이 높지만, 압축한 결과가 수백 MB인 캐시를 만들면 올리고 내리는 시간이 다시 받는 시간을 넘길 수 있습니다.
어느 쪽인지는 캐시를 붙인 전후의 잡 전체 시간을 비교해야 알 수 있습니다.
&lt;a href=&quot;github-actions-tuning-cases.html&quot;&gt;다음 글&lt;/a&gt;에 캐시를 붙여도 잡 전체 시간이 줄지 않은 실제 측정 사례를 실었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;캐시_한도와_스코프&quot;&gt;4.2. 캐시 한도와 스코프&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시를 설계할 때 알아야 할 GitHub의 제약이 세 가지 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;저장소당 기본 한도는 10GB입니다. 한도를 넘으면 기존 캐시가 삭제됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;7일 넘게 접근되지 않은 캐시는 삭제됩니다. 한 달에 한 번 도는 워크플로는 매번 캐시가 없는 상태로 시작합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;워크플로 실행은 현재 브랜치와 기본 브랜치의 캐시를 복원할 수 있습니다. 풀 리퀘스트에서 실행됐다면 base 브랜치의 캐시도 복원합니다. 자식 브랜치나 형제 브랜치의 캐시는 쓰지 못합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;풀 리퀘스트에는 규칙이 하나 더 붙습니다.
풀 리퀘스트에서 만든 캐시는 병합 참조(&lt;code&gt;refs/pull/&amp;#8230;&amp;#8203;/merge&lt;/code&gt;)에 묶이므로, 그 풀 리퀘스트를 다시 실행할 때만 복원됩니다.
기본 브랜치나 다른 풀 리퀘스트는 그 캐시를 쓰지 못합니다.
따라서 캐시를 처음 채우는 일은 기본 브랜치의 워크플로가 맡아야 하고, 풀 리퀘스트는 거기서 복원해 쓰는 구조가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;잡_분리와_매트릭스의_손익&quot;&gt;5. 잡 분리와 매트릭스의 손익&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한 잡 안에서 순서대로 실행하던 작업을 여러 잡으로 나누면 병렬로 돌아갑니다.
매트릭스를 쓰면 같은 작업을 여러 입력으로 반복하는 잡을 한 번에 선언할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [&quot;3.10&quot;, &quot;3.11&quot;, &quot;3.12&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;나눠진 잡들은 동시에 시작하므로, 러너 사용 시간의 합은 잡들의 합이지만 실제로 기다리는 시간은 가장 느린 잡의 시간으로 줄어듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기에 두 가지 대가가 따릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째, 잡마다 러너 준비 비용을 다시 냅니다.
&lt;code&gt;Set up job&lt;/code&gt;, 체크아웃, setup 액션까지 실제 작업 전에 대개 10~30초를 씁니다.
잡을 셋으로 나누면 이 비용을 세 번 냅니다.
전체가 30초짜리인 워크플로를 나누면 오히려 느려집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 잡 사이에 데이터를 넘기려면 아티팩트 업로드와 다운로드가 필요합니다.
한 잡 안에서는 파일 시스템에 그대로 두면 되던 빌드 결과물을, 잡을 나누면 올렸다가 다시 받아야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 잡을 나눌 만한 경우는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;서로 결과를 주고받지 않는 작업이다. 예: 린트, 테스트, 문서 빌드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;각 작업이 러너 준비 비용(10~30초)보다 충분히 오래 걸린다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;매트릭스처럼 같은 작업을 다른 입력으로 반복한다&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;needs&lt;/code&gt;로 잡 사이 의존을 걸면 병렬성이 사라지므로, 정말 앞 잡의 결과가 필요할 때만 씁니다.
매트릭스에서는 &lt;code&gt;fail-fast&lt;/code&gt;가 기본값 &lt;code&gt;true&lt;/code&gt;라, 한 조합이 실패하면 나머지 조합이 취소됩니다.
실패를 빨리 알고 러너 시간을 아끼려면 그대로 두고, 어느 버전에서 깨지는지 한 번에 다 보려면 &lt;code&gt;false&lt;/code&gt;로 바꿉니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;러너_사양과_종류_선택&quot;&gt;6. 러너 사양과 종류 선택&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 워크플로도 저장소 공개 여부에 따라 다른 사양에서 돕니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;구분&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;공개 저장소&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;비공개 저장소&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux 표준 러너&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;4 vCPU, 16GB RAM&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2 vCPU, 8GB RAM&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;디스크&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;14GB SSD&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;14GB SSD&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;비공개 저장소의 표준 러너는 2코어입니다.
Gradle의 병렬 빌드나 &lt;code&gt;go test -p&lt;/code&gt;처럼 코어 수에 비례하는 작업은 여기서 기대만큼 빨라지지 않습니다.
공개 저장소에서 측정한 시간을 비공개 저장소에 그대로 대입하면 안 되는 이유이기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;선택할 수 있는 러너는 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;arm64 표준 러너&lt;/strong&gt;: &lt;code&gt;ubuntu-24.04-arm&lt;/code&gt; 같은 레이블로 씁니다. 2025년 8월에 공개 저장소에서 정식 출시됐고, 2026년 1월부터 비공개 저장소에서도 쓸 수 있습니다. x64 대비 분당 단가가 낮고, 워크로드에 따라 더 빠릅니다. 다만 네이티브 확장을 빌드하는 프로젝트는 arm64용 휠이나 바이너리가 있는지 확인해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Larger runner&lt;/strong&gt;: 코어를 8개 이상으로 늘린 유료 러너입니다. 공개 저장소에서도 항상 과금되므로, 늘어난 코어를 실제로 쓰는 작업인지 먼저 확인해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Self-hosted runner&lt;/strong&gt;: 캐시와 도구를 디스크에 남겨 둘 수 있어 준비 시간이 짧습니다. 대신 러너를 직접 관리해야 하고, 공개 저장소에서는 포크의 풀 리퀘스트가 임의의 코드를 실행할 수 있어 보안 위험이 큽니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;운영체제 선택도 시간과 비용에 영향을 줍니다.
표준 러너의 분당 단가는 Linux 2코어가 $0.006, Windows 2코어가 $0.010, macOS 3~4코어가 $0.062입니다.
공개 저장소에서는 표준 러너가 무료지만, 비공개 저장소에서 macOS 러너를 쓰면 같은 시간에 Linux의 10배가 넘게 청구됩니다.
macOS 러너는 애플 플랫폼용 빌드에만 쓰고, 나머지는 Linux에 두는 편이 낫습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;체크아웃과_아티팩트_다듬기&quot;&gt;7. 체크아웃과 아티팩트 다듬기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;남은 두 가지는 개별 효과가 크지는 않지만 설정이 간단합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;actions/checkout&lt;/code&gt;의 &lt;code&gt;fetch-depth&lt;/code&gt; 기본값은 &lt;code&gt;1&lt;/code&gt;이라 최신 커밋 하나만 받습니다.
&lt;code&gt;0&lt;/code&gt;으로 바꾸면 전체 이력을 받는데, 커밋이 수만 개인 저장소에서는 이 단계만 수십 초가 걸립니다.
릴리스 노트 생성처럼 이력이 정말 필요한 잡에만 &lt;code&gt;0&lt;/code&gt;을 지정하고, 나머지는 기본값을 두면 됩니다.
모노레포에서 일부 디렉터리만 필요하다면 &lt;code&gt;sparse-checkout&lt;/code&gt;으로 받는 범위를 줄일 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;- uses: actions/checkout@v4
  with:
    sparse-checkout: |
      services/api
      libs/common&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아티팩트는 필요할 때만 올립니다.
예를 들어 테스트 리포트를 실패했을 때만 업로드하면, 성공한 실행에서는 이 단계가 0초로 끝납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;- name: Upload test reports on failure
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: test-reports
    path: build/reports/tests/test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;actions/upload-artifact&lt;/code&gt;는 v4에서 업로드·다운로드가 최대 10배 빨라졌으므로, v3 이하를 쓰고 있다면 올리는 편이 좋습니다.
다만 v4부터 아티팩트가 워크플로가 아닌 잡 단위로 묶이므로, 매트릭스 잡에서 같은 이름으로 올리면 충돌합니다.
이름에 매트릭스 값을 붙여 구분해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;github_enterprise_server에서_달라지는_손익_계산&quot;&gt;8. GitHub Enterprise Server에서 달라지는 손익 계산&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지의 계산은 github.com을 전제로 했습니다.
앞의 조건 평가 시점 절에서 파일 수와 조건식의 개수는 걱정할 요인이 아니라고 했는데, 그 전제는 트리거 평가와 실행 생성이 GitHub의 공용 인프라에서 일어난다는 것이었습니다.
설치형인 GitHub Enterprise Server(GHES)에서는 이 전제가 달라집니다.
webhook 접수부터 트리거 평가, 실행 생성, 잡 큐잉까지 파이프라인 전체가 사내 인스턴스의 자원으로 돌아가기 때문입니다.
GitHub은 GHES에서 Actions를 켜는 데에 최소 8 vCPU와 64GB 메모리를 요구하고, 연결하는 러너 수에 따라 권장 사양을 올려 잡습니다(예: 32 vCPU·160GB 메모리의 최대 연결 러너 수는 2,700대).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 github.com에서는 유지보수성 관점에서 추천했던 파일 정리가 GHES에서는 자기 인스턴스의 용량 절감이 됩니다.
안 쓰는 워크플로를 삭제·비활성화하고, 미러링이나 봇의 대량 푸시처럼 워크플로가 필요 없는 이벤트를 줄이면, 그만큼 인스턴스가 처리할 평가와 실행 생성이 줄어듭니다.
&lt;code&gt;schedule&lt;/code&gt;의 정각 분산도 의미가 달라집니다.
github.com에서는 GitHub의 혼잡을 피하는 요령이었지만, GHES에서는 조직 전체의 cron이 정각에 몰리면 내 인스턴스에 부하 스파이크가 생깁니다.
다만 GHES에서도 트리거 평가 자체가 병목이라는 근거가 따로 있지는 않으므로, 이런 정리는 지연을 고치는 처방이라기보다 인스턴스 용량을 아끼는 예방에 가깝습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;러너와 스토리지의 전제도 다릅니다.
GHES에는 GitHub 호스티드 러너가 없어 셀프 호스티드 러너만 쓸 수 있습니다.
잡이 큐에서 기다리는 시간이 전적으로 사내 러너 풀의 용량에 달려 있으므로, 러너 사양 절에서 본 표준 러너의 사양·단가 비교는 GHES에 적용되지 않습니다.
로그·아티팩트·캐시는 별도로 준비한 외부 blob storage(S3, Azure Blob 등)에 저장되므로, 캐시의 손익도 그 스토리지의 성능에 따라 달라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;측정 수단은 오히려 늘어납니다.
github.com에서는 실행 생성 앞 구간이 블랙박스지만, GHES 관리자는 관리 콘솔의 모니터 대시보드에서 CPU·메모리 포화, 백그라운드 잡 큐 적체, Actions 관련 지표를 직접 볼 수 있습니다.
실행 시작이 느릴 때 워크플로를 고치기 전에 인스턴스 쪽 병목부터 확인할 수 있으므로, 측정을 먼저 하라는 이 글의 순서는 GHES에서도 그대로 적용됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;9. 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지 살펴본 기법을 적용 순서대로 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 42.8571%;&quot;&gt;
&lt;col style=&quot;width: 42.8572%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;순서&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;확인할 것&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;주의할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;gh run view&lt;/code&gt;로 단계별 시간과 대기 시간 측정&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;대기가 길면 워크플로 내부 최적화는 효과가 없다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;paths&lt;/code&gt;·&lt;code&gt;paths-ignore&lt;/code&gt;로 실행 자체를 제거&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;필수 상태 검사에는 쓰지 않는다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;3&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;concurrency&lt;/code&gt;로 중복 실행 취소&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;배포 워크플로에는 &lt;code&gt;cancel-in-progress&lt;/code&gt;를 켜지 않는다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;4&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;setup 액션의 내장 캐시 적용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;복원 + 저장 시간이 다시 받는 시간보다 클 수 있다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;잡 분리와 매트릭스로 병렬화&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;잡마다 준비 비용 10~30초를 다시 낸다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;6&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;러너 사양과 운영체제 선택&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;비공개 저장소는 2코어, macOS는 Linux의 10배 단가&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1번을 건너뛰고 2번부터 시작하면, 병목이 다른 곳에 있는 워크플로에서 헛수고를 하게 됩니다.
이 순서대로 제 저장소 다섯 곳에 적용하며 측정한 기록을 다음 글에 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;github-actions-tuning-cases.html&quot;&gt;GitHub Actions 워크플로 튜닝: 저장소 다섯 곳의 적용 사례 →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;10. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;GitHub 공식 문서&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows&quot;&gt;Events that trigger workflows&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/limits&quot;&gt;GitHub Actions limits&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution&quot;&gt;Using conditions to control job execution&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/concepts/runners/communicating-with-self-hosted-runners&quot;&gt;Communicating with self-hosted runners&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching&quot;&gt;Dependency caching&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency&quot;&gt;Concurrency&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/runners/github-hosted-runners&quot;&gt;GitHub-hosted runners reference&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-actions&quot;&gt;About billing for GitHub Actions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/concepts/metrics&quot;&gt;About GitHub Actions metrics&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks&quot;&gt;Troubleshooting required status checks&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/enterprise-server@latest/admin/github-actions/getting-started-with-github-actions-for-your-enterprise/getting-started-with-github-actions-for-github-enterprise-server&quot;&gt;Getting started with GitHub Actions for GitHub Enterprise Server&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/enterprise-server@latest/admin/monitoring-and-managing-your-instance/monitoring-your-instance/accessing-the-monitor-dashboard&quot;&gt;Accessing the monitor dashboard (GHES)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;GitHub Changelog&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/changelog/2025-08-07-arm64-hosted-runners-for-public-repositories-are-now-generally-available/&quot;&gt;arm64 hosted runners for public repositories are now generally available&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/changelog/2026-01-29-arm64-standard-runners-are-now-available-in-private-repositories/&quot;&gt;arm64 standard runners are now available in private repositories&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/changelog/2024-02-12-github-actions-artifacts-v4-is-now-generally-available/&quot;&gt;GitHub Actions Artifacts v4 is now Generally Available&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;장애 보고서 (GitHub Status·GitHub Blog)&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.githubstatus.com/incidents/qcvjkzcs7j74&quot;&gt;2026년 8월 6일 Actions 장애 원인 분석 - GitHub Status&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-availability-report-march-2021/&quot;&gt;GitHub Availability Report: March 2021&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-availability-report-august-2023/&quot;&gt;GitHub Availability Report: August 2023&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-availability-report-october-2025/&quot;&gt;GitHub Availability Report: October 2025&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-availability-report-march-2026/&quot;&gt;GitHub Availability Report: March 2026&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-availability-report-may-2026/&quot;&gt;GitHub Availability Report: May 2026&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/github-availability-report-june-2026/&quot;&gt;GitHub Availability Report: June 2026&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/company-news/addressing-githubs-recent-availability-issues-2/&quot;&gt;Addressing GitHub&amp;#8217;s recent availability issues&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;외부 블로그&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://depot.dev/blog/github-actions-runner-architecture-part-1-the-listener&quot;&gt;GitHub Actions Runner Architecture Part 1: The Listener - Depot&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Call by value vs Call by reference</title>
      <link>https://blog.benelog.net//call-by-value-vs-call-by-reference.html</link>
      <pubDate>Sun, 23 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">call-by-value-vs-call-by-reference.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#call_by_value와_call_by_reference&quot;&gt;Call by value와 call by reference&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#pass_by_와_call_by_용어&quot;&gt;Pass by ~와 call by ~ 용어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#call_by_sharing&quot;&gt;Call by sharing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#java&quot;&gt;Java&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#swap_메서드의_실행_결과&quot;&gt;swap 메서드의 실행 결과&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#파라미터_재할당이_호출자_변수에_반영되지_않는_이유&quot;&gt;파라미터 재할당이 호출자 변수에 반영되지 않는 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#오해의_근원인_객체_상태_변경&quot;&gt;오해의 근원인 객체 상태 변경&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#java의_reference와_용어의_혼란&quot;&gt;Java의 reference와 용어의 혼란&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#메서드_호출마다_새로_만들어지는_프레임&quot;&gt;메서드 호출마다 새로 만들어지는 프레임&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#kotlin&quot;&gt;Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#javascript&quot;&gt;JavaScript&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#javascript_the_definitive_guide의_call_by_reference_서술&quot;&gt;JavaScript: The Definitive Guide의 call by reference 서술&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c&quot;&gt;C&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c_2&quot;&gt;C&amp;#43;&amp;#43;&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#포인터_파라미터와_참조_파라미터의_차이&quot;&gt;포인터 파라미터와 참조 파라미터의 차이&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c_표준이_기술하는_참조&quot;&gt;C&amp;#43;&amp;#43; 표준이 기술하는 참조&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c_3&quot;&gt;C#&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#python&quot;&gt;Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#go&quot;&gt;Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rust&quot;&gt;Rust&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ada_call_by_reference와_copy_restore의_차이&quot;&gt;Ada: call by reference와 copy-restore의 차이&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Java의 메서드 호출에서 primitive type은 call by value로, 객체는 call by reference로 인자가 전달된다&quot;는 설명을 종종 접합니다.
그러나 Java에는 call by value만 있습니다.
메서드에 객체를 전달할 때 복사되는 값이 &apos;객체를 가리키는 참조값&amp;#8217;일 뿐입니다.
이 글에서는 두 용어의 정의를 정리하고, Java, Kotlin, JavaScript, C, C&amp;#43;&amp;#43;, C#, Python, Go, Rust, Ada가 인자를 어떻게 전달하는지 비교합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;call_by_value와_call_by_reference&quot;&gt;Call by value와 call by reference&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Call by value&lt;/strong&gt;는 인자로 넘긴 값으로 파라미터라는 별도의 변수를 초기화하는 방식입니다. 대개 값을 복사하며, Rust처럼 소유권을 옮기는 언어도 있습니다. 함수 안에서 파라미터에 다른 값을 대입해도 호출한 쪽의 변수는 영향을 받지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Call by reference&lt;/strong&gt;는 변수 자체를 전달하는 방식입니다. 파라미터는 호출한 쪽 변수의 별칭(alias)이 되므로, 함수 안에서 파라미터에 새 값을 대입하면 호출한 쪽의 변수도 함께 바뀝니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 정의는 프로그래밍 언어론의 표준적인 설명을 따릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Compilers: Principles, Techniques, and Tools&lt;/strong&gt; 2판은 1.6.6절에서 두 방식을 다음과 같이 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In call-by-value, the actual parameter is evaluated (if it is an expression) or copied (if it is a variable). The value is placed in the location belonging to the corresponding formal parameter of the called procedure. This method is used in C and Java, and is a common option in C++, as well as in most other languages. &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In call-by-reference, the address of the actual parameter is passed to the callee as the value of the corresponding formal parameter. Uses of the formal parameter in the code of the callee are implemented by following this pointer to the location indicated by the caller. Changes to the formal parameter thus appear as changes to the actual parameter.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Call by value에서는 실제 파라미터가 표현식이면 평가하고, 변수이면 복사한다. 그 값은 호출된 프로시저에서 대응하는 형식 파라미터의 저장 위치에 놓인다. 이 방식은 C와 Java에서 쓰이며, C&amp;#43;&amp;#43;와 대부분의 다른 언어에서도 흔히 쓰는 선택지다. &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Call by reference에서는 실제 파라미터의 주소가 대응하는 형식 파라미터의 값으로 호출된 쪽에 전달된다. 호출된 쪽의 코드에서 형식 파라미터를 쓰는 부분은 이 포인터를 따라 호출한 쪽이 가리킨 위치에 접근하도록 구현된다. 따라서 형식 파라미터의 변경은 실제 파라미터의 변경으로 나타난다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman&lt;br&gt;
&lt;cite&gt;Compilers: Principles, Techniques, and Tools 2판 1.6.6절&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인용문의 실제 파라미터(actual parameter)는 이 글의 인자에, 형식 파라미터(formal parameter)는 이 글의 파라미터에 해당합니다. 이 책은 1977년의 전신 &lt;strong&gt;Principles of Compiler Design&lt;/strong&gt;부터 대학 컴파일러 강의의 기본 교재로 쓰여 온 표준 교과서입니다. 표지 그림 때문에 Dragon Book으로도 불립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/call-by-value/dragon-book-2nd.jpg&quot; alt=&quot;Compilers: Principles, Techniques, and Tools 2판 표지&quot; width=&quot;200&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. Compilers: Principles, Techniques, and Tools 2판 표지 (이미지 출처: &lt;a href=&quot;https://openlibrary.org/isbn/9780321486813&quot;&gt;Open Library&lt;/a&gt;)&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Christopher Strachey는 1967년 강의를 정리한 &lt;a href=&quot;https://www.cs.cmu.edu/~crary/819-f09/Strachey67.pdf&quot;&gt;Fundamental Concepts in Programming Languages&lt;/a&gt; 3.4.2절에서 이 구분을 L-value와 R-value로 설명했습니다. 파라미터에 표현식의 R-value를 넘기는 방식을 call by value, L-value를 넘기는 방식을 call by reference라고 불렀습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When the function is used (or called or applied) we write f[ε] where ε can be an expression. If we are using a referentially transparent language all we require to know about the expression ε in order to evaluate f[ε] is its value. There are, however, two sorts of value, so we have to decide whether to supply the R-value or the L-value of ε to the function f. Either is possible, so that it becomes a part of the definition of the function to specify for each of its bound variables (also called its formal parameters) whether it requires an R-value or an L-value. These alternatives will also be known as calling a parameter by value (R-value) or reference (L-value).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;함수를 사용할(호출하거나 적용할) 때는 f[ε]라고 쓰며, ε는 표현식일 수 있다. 참조 투명한 언어라면 f[ε]를 평가하는 데 표현식 ε에 대해 알아야 할 것은 그 값뿐이다. 그러나 값에는 두 종류가 있으므로 함수 f에 ε의 R-value를 넘길지 L-value를 넘길지 정해야 한다. 어느 쪽도 가능하므로, 함수의 각 속박 변수(형식 파라미터라고도 한다)가 R-value를 요구하는지 L-value를 요구하는지 정하는 일이 함수 정의의 일부가 된다. 이 두 가지를 파라미터를 값(R-value)으로 호출하는 방식과 참조(L-value)로 호출하는 방식이라고도 부른다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Christopher Strachey&lt;br&gt;
&lt;cite&gt;Fundamental Concepts in Programming Languages 3.4.2절&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;L-value는 대입문의 왼쪽(Left)에 놓인 표현식이 뜻하는 저장 위치이고, R-value는 오른쪽(Right)에 놓인 표현식이 뜻하는 그 위치에 담긴 값입니다. 이 강의록은 L-value와 R-value, first-class 객체 같은 용어의 출처로 지금도 인용되는 프로그래밍 언어론의 고전입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 방식을 구분하는 실용적인 기준은 한 문장으로 요약할 수 있습니다. &lt;strong&gt;함수 안에서 파라미터에 새 값을 대입했을 때 호출한 쪽의 변수가 바뀌는가?&lt;/strong&gt; 바뀌면 call by reference이고, 바뀌지 않으면 call by value입니다. 이 기준은 이 글처럼 두 방식만 비교하고, 다시 대입할 수 있는 변수를 인자로 넘기며, 파라미터에도 대입할 수 있다고 가정할 때 성립합니다. Call by copy-restore나 call by name 등 다른 평가 전략까지 포함하면 이 질문만으로 모든 인자 전달 방식을 분류할 수는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또한 이 기준에서 말하는 대입은 &lt;code&gt;targetTv = new Tv()&lt;/code&gt;처럼 파라미터 변수 자체에 다른 값을 넣는 것입니다. 파라미터가 객체를 가리킨다면 다른 객체를 가리키게 바꾸는 셈입니다. &lt;code&gt;targetTv.setChannel(&quot;Rock&quot;)&lt;/code&gt;처럼 파라미터가 가리키는 객체의 내부 상태를 바꾸는 것은 별개의 문제입니다. 뒤에서 볼 오해는 이 둘을 혼동하는 것에서 생깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;pass_by_와_call_by_용어&quot;&gt;Pass by ~와 call by ~ 용어&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Call by value와 call by reference는 pass by value, pass by reference라고도 부릅니다. &lt;code&gt;call by ~&lt;/code&gt;는 1960년에 발표된 ALGOL 60 보고서부터 쓰인 형태입니다. &lt;a href=&quot;https://www.masswerk.at/algol60/report.htm&quot;&gt;1963년 개정 보고서&lt;/a&gt;의 4.7.3절이 파라미터 전달 방식을 규정하며 4.7.3.1에 &apos;Value assignment (call by value)&apos;, 4.7.3.2에 &apos;Name replacement (call by name)&apos;이라는 제목을 붙였습니다. 실무와 교재에서는 &lt;code&gt;pass by ~&lt;/code&gt;도 널리 쓰입니다. 이 글에서는 &lt;code&gt;call by ~&lt;/code&gt;로 통일했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;call_by_sharing&quot;&gt;Call by sharing&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java, JavaScript, Python처럼 호출자 변수 자체가 아니라 객체를 공유하는 인자 전달 동작을 &lt;strong&gt;call by sharing&lt;/strong&gt;이라고 부르기도 합니다. Barbara Liskov를 비롯한 설계팀이 1970년대 중반에 만든 언어인 &lt;a href=&quot;https://csg.csail.mit.edu/pubs/memos/Memo-112/memo112-1a.pdf&quot;&gt;CLU의 설계 문서&lt;/a&gt;에서 쓴 용어입니다. 뒤에서 살펴볼 Java, Kotlin, JavaScript, Python의 객체 인자 동작과 C#의 class 인스턴스 전달이 모두 여기에 해당합니다. Python 튜토리얼이 쓴 call by object reference도 같은 뜻입니다. Go의 맵이나 포인터, C의 포인터를 전달할 때도 같은 구조가 나타납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Call by value라고 하면 &apos;객체가 통째로 복사된다&amp;#8217;는 오해를, call by reference라고 하면 &apos;재할당도 호출한 쪽 변수에 반영된다&amp;#8217;는 오해를 낳을 수 있습니다. Call by sharing은 이런 오해를 피하는 데 좋은 용어입니다. 뒤에서 인용할 MDN의 JavaScript 문서처럼 이 용어를 쓰는 자료도 있지만, 교재와 언어 사양에서 표준 용어로 자리 잡지는 않았습니다. 그래서 이 글에서는 call by value와 call by reference 두 가지로만 구분해서 설명을 이어 가겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;java&quot;&gt;Java&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;swap_메서드의_실행_결과&quot;&gt;swap 메서드의 실행 결과&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 코드의 출력으로 Java의 전달 방식을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;swap 메서드 테스트&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class ReferenceTest {
    public static void main(String[] args) {
        String x1 = &quot;Hello&quot;;
        String y1 = &quot;World&quot;;
        swap(x1, y1); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
        System.out.println(x1); // Hello

        int x2 = 1;
        int y2 = 2;
        swap(x2, y2); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
        System.out.println(x2); // 1
    }

    static void swap(Object x, Object y) {
        Object temp = x;
        x = y; // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
        y = temp;
    }

    static void swap(int x, int y) {
        int temp = x;
        x = y;
        y = temp;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;x1&lt;/code&gt;과 &lt;code&gt;y1&lt;/code&gt;에 담긴 참조값의 복사본이 파라미터 &lt;code&gt;x&lt;/code&gt;와 &lt;code&gt;y&lt;/code&gt;에 대입됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;int 값이 &lt;code&gt;swap(int, int)&lt;/code&gt; 오버로드의 파라미터에 복사됩니다. 이 오버로드가 없으면 &lt;code&gt;Object&lt;/code&gt; 파라미터에 오토박싱된 Integer 객체의 참조값이 전달되는데, 결과는 같습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;메서드 안에서 파라미터 &lt;code&gt;x&lt;/code&gt;와 &lt;code&gt;y&lt;/code&gt;에 서로의 값을 대입해도, 호출한 쪽의 &lt;code&gt;x1&lt;/code&gt;, &lt;code&gt;y1&lt;/code&gt;, &lt;code&gt;x2&lt;/code&gt;, &lt;code&gt;y2&lt;/code&gt;는 바뀌지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉, 앞에서 정리한 기준대로라면 Java는 primitive type이든 객체든 call by value로 동작합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;파라미터_재할당이_호출자_변수에_반영되지_않는_이유&quot;&gt;파라미터 재할당이 호출자 변수에 반영되지 않는 이유&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드 파라미터로 객체를 전달하는 경우를 더 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;메서드 안에서 파라미터 재할당&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class Tv {
    private String channel;

    void setChannel(String channel) { this.channel = channel; }
    String getChannel() { return channel; }
}

public class ReassignTest {
    public static void main(String[] args) {
        var myTv = new Tv();
        myTv.setChannel(&quot;Classic&quot;);

        reassign(myTv); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
        System.out.println(myTv.getChannel()); // Classic
    }

    static void reassign(Tv targetTv) {
        targetTv = new Tv(); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
        targetTv.setChannel(&quot;Rock&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;main 메서드의 &lt;code&gt;myTv&lt;/code&gt;에 담긴 참조값이 복사되어 reassign 메서드의 파라미터 &lt;code&gt;targetTv&lt;/code&gt;에 대입됩니다. 두 변수는 같은 객체를 가리키지만 서로 다른 변수입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파라미터 &lt;code&gt;targetTv&lt;/code&gt;에 새 객체를 대입합니다. 파라미터 &lt;code&gt;targetTv&lt;/code&gt;만 새 객체를 가리키게 되고, main 메서드의 &lt;code&gt;myTv&lt;/code&gt;는 원래 객체를 그대로 가리킵니다. 그래서 출력은 &lt;code&gt;Classic&lt;/code&gt;입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/call-by-value/java-reassign.png&quot; alt=&quot;참조값이 복사된 뒤 파라미터만 새 객체를 가리키게 되는 과정&quot; width=&quot;700&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;오해의_근원인_객체_상태_변경&quot;&gt;오해의 근원인 객체 상태 변경&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 파라미터를 재할당하지 않고 파라미터가 가리키는 객체의 내부 상태를 바꾸면, 호출한 쪽의 변수로 접근해도 바뀐 상태가 그대로 확인됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;메서드 안에서 파라미터 재할당 없이 객체 상태 변경&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class ChangeStateTest {
    public static void main(String[] args) {
        var myTv = new Tv();
        myTv.setChannel(&quot;Classic&quot;);

        changeState(myTv); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
        System.out.println(myTv.getChannel()); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
    }

    static void changeState(Tv targetTv) {
        targetTv.setChannel(&quot;Rock&quot;); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;앞의 예제와 같이 &lt;code&gt;myTv&lt;/code&gt;에 담긴 참조값이 복사되어 changeState 메서드의 파라미터 &lt;code&gt;targetTv&lt;/code&gt;에 대입됩니다. 두 변수는 같은 객체를 가리킵니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파라미터 &lt;code&gt;targetTv&lt;/code&gt;를 재할당하지 않고, 파라미터가 가리키는 객체의 채널을 &lt;code&gt;Rock&lt;/code&gt;으로 바꿉니다. main 메서드의 &lt;code&gt;myTv&lt;/code&gt;가 가리키는 바로 그 객체입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;main 메서드의 &lt;code&gt;myTv&lt;/code&gt;로 채널을 읽으면 &lt;code&gt;Rock&lt;/code&gt;이 출력됩니다. 변수는 바뀌지 않았지만 변수가 가리키는 객체의 상태가 바뀌었기 때문입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/call-by-value/java-mutation.png&quot; alt=&quot;두 변수가 같은 객체를 가리키므로 내부 상태 변경이 호출자 변수로 접근해도 확인되는 상황&quot; width=&quot;700&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;호출자 변수로 읽어도 바뀐 상태가 보이니 &quot;객체는 call by reference로 전달된다&quot;는 설명이 그럴듯하게 들립니다.
그러나 앞에서 설명했듯이, 두 방식을 구분하는 실용적인 기준은 파라미터에 대입한 값이 호출자 변수에 반영되는가입니다.
호출자 변수로 바뀐 상태가 보이는 것은 두 변수가 같은 객체를 가리키기 때문일 뿐이고, 전달되는 것은 여전히 참조값의 복사본입니다. 즉 call by value입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Head First Java&lt;/strong&gt;에서는 객체 참조를 TV 리모컨에 비유해서 설명합니다. 객체를 전달하면 TV(객체)가 복사되는 것이 아니라 그 TV를 조작할 수 있는 리모컨(참조값)이 복사됩니다. 복사된 리모컨으로 채널을 바꾸면(객체 상태 변경) 원래 리모컨을 가진 사람에게도 바뀐 채널이 보입니다. 복사된 리모컨을 새 TV에 연결하면(재할당, &lt;code&gt;targetTv = new Tv()&lt;/code&gt;) 그 리모컨으로는 원래 TV를 조작할 수 없고, 원래 리모컨은 여전히 원래 TV에 연결되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;java의_reference와_용어의_혼란&quot;&gt;Java의 reference와 용어의 혼란&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java의 창시자 James Gosling은 공저한 &lt;strong&gt;The Java Programming Language&lt;/strong&gt;에서 이 오해를 직접 지적했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Some people will say incorrectly that objects are passed &quot;by reference.&quot; &amp;#8230;&amp;#8203; The Java programming language does not pass objects by reference; it passes object references by value.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 사람들은 객체가 &apos;참조로(by reference)&apos; 전달된다고 잘못 말한다. &amp;#8230;&amp;#8203; Java 프로그래밍 언어는 객체를 참조로 전달하지 않는다. 객체의 참조를 값으로 전달한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Ken Arnold, James Gosling, David Holmes&lt;br&gt;
&lt;cite&gt;The Java Programming Language 4판&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/call-by-value/java-programming-language-4th.jpg&quot; alt=&quot;The Java Programming Language 4판 표지&quot; width=&quot;200&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. The Java Programming Language 4판 표지 (이미지 출처: &lt;a href=&quot;https://openlibrary.org/isbn/9780321349804&quot;&gt;Open Library&lt;/a&gt;)&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최신 공식 Java 학습 자료인 &lt;a href=&quot;https://dev.java/learn/classes-objects/calling-methods-constructors/&quot;&gt;Dev.java&lt;/a&gt;에서도 같은 내용을 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Reference data type parameters, such as objects, are also passed into methods by value. &amp;#8230;&amp;#8203; when the method returns, the passed-in reference still references the same object as before.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;객체 같은 참조 데이터 타입 파라미터도 메서드에 값으로 전달된다. &amp;#8230;&amp;#8203; 메서드가 반환될 때 전달된 참조는 여전히 이전과 같은 객체를 참조한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Dev.java&lt;br&gt;
&lt;cite&gt;Calling Methods and Constructors&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 인용한 Dragon Book도 같은 절에서 Java는 call by value만 쓴다고 밝힙니다. 그런데 이어지는 call by reference 항목에서는 Java가 기본 타입이 아닌 값에는 call by reference를 쓰는 것처럼 동작한다고 적었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Even though Java uses call-by-value exclusively, whenever we pass the name of an object to a called procedure, the value received by that procedure is in effect a pointer to the object. Thus, the called procedure is able to affect the value of the object itself. &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As we noted when discussing call-by-value, languages such as Java solve the problem of passing arrays, strings, or other objects by copying only a reference to those objects. The effect is that Java behaves as if it used call-by-reference for anything other than a basic type such as an integer or real.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java는 call by value만 쓰지만, 호출되는 프로시저에 객체의 이름을 넘기면 그 프로시저가 받는 값은 사실상 그 객체를 가리키는 포인터다. 따라서 호출된 프로시저는 객체 자체의 값에 영향을 줄 수 있다. &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Call by value를 다루면서 언급했듯이, Java 같은 언어는 배열, 문자열, 그 밖의 객체를 넘기는 문제를 그 객체에 대한 참조만 복사하는 방식으로 해결한다. 그 결과 Java는 정수나 실수 같은 기본 타입이 아닌 모든 것에 call by reference를 쓰는 것처럼 동작한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman&lt;br&gt;
&lt;cite&gt;Compilers: Principles, Techniques, and Tools 2판 1.6.6절&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;call by reference를 쓰는 것처럼 동작한다(behaves as if)&apos;는 객체를 통째로 복사하지 않고, 호출된 메서드에서 바꾼 객체 상태가 호출한 쪽에서도 보인다는 효과를 가리킵니다. 이 책이 앞에서 밝혔듯이 실제 전달 방식은 call by reference가 아니라는 전제를 깐 표현입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;reference&amp;#8217;라는 용어도 Java가 객체를 call by reference로 전달한다는 오해를 키운 것으로 보입니다. Java에서는 객체를 가리키는 값을 &apos;reference&amp;#8217;라고 부르는데, 이 단어가 &apos;call by reference&amp;#8217;의 reference와 겹치기 때문입니다. 더 정확하게 표현하면 &lt;strong&gt;Java는 객체를 가리키는 참조값을 call by value로 전달합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java의 reference는 객체를 식별하고 접근하는 값입니다. 포인터처럼 이해할 수 있습니다.
다음의 Java 객체 접근 동작은&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Java 객체 접근&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;var myTv = new Tv();
myTv.setChannel(&quot;Rock&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C&amp;#43;&amp;#43; 포인터를 쓴 다음 코드와 비슷합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;C&amp;#43;&amp;#43; 포인터&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;cpp&quot;&gt;Tv *myTv = new Tv();
myTv-&amp;gt;setChannel(&quot;Rock&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java의 &lt;code&gt;myTv&lt;/code&gt; 변수는 객체 자체가 아니라 객체를 가리키는 값을 담고 있습니다. &lt;code&gt;myTv&lt;/code&gt;를 다른 메서드의 인자로 넘기면 이 참조값이 복사되어 전달됩니다. 다만 위 C&amp;#43;&amp;#43; 코드는 객체 접근과 포인터 재할당을 설명하기 위한 비유이며, 객체의 수명 관리나 참조의 실제 표현까지 같다는 뜻은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이와 관련된 JLS(Java Language Specification)의 서술을 보겠습니다. JLS도 &apos;포인터&amp;#8217;라는 단어를 씁니다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se26/html/jls-4.html#jls-4.3.1&quot;&gt;4.3.1. Objects&lt;/a&gt; 절은 참조값을 객체를 가리키는 포인터로 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참조값(흔히 그냥 참조라고 부른다)은 이 객체들을 가리키는 포인터이고, 여기에 어떤 객체도 가리키지 않는 특별한 null 참조가 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Java Language Specification&lt;br&gt;
&lt;cite&gt;4.3.1. Objects&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 이는 언어 수준의 의미를 설명하는 표현이지, 참조값이 원시 메모리 주소와 같은 형식이라고 보장하는 문장은 아닙니다. &lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-2.html#jvms-2.7&quot;&gt;JVMS(Java Virtual Machine Specification) 2.7&lt;/a&gt;은 객체의 내부 구조를 특정하지 않으며, 일부 구현에서는 참조가 객체 데이터가 아니라 핸들을 가리킬 수도 있다고 설명합니다. 여기서 핸들은 참조와 객체 데이터 사이에 놓인 작은 중간 구조입니다. 참조는 핸들을 가리키고, 핸들 안의 포인터가 다시 객체 데이터와 메서드 테이블을 가리킵니다. 참조값이 객체가 놓인 메모리 주소 그 자체가 아닐 수도 있다는 뜻입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;핸들에 관한 이 설명은 &lt;a href=&quot;https://web.archive.org/web/20001213164500/http://java.sun.com/docs/books/vmspec/html/Overview.doc.html&quot;&gt;JVMS 초판(1997)&lt;/a&gt;에서 &quot;Sun의 현재 구현&quot;을 설명하던 것입니다. 2판에서 &quot;Sun의 일부 구현&quot;으로, Oracle 인수 뒤의 판에서 &quot;Oracle의 일부 구현&quot;으로 바뀌었을 뿐 내용은 같습니다. 초판 당시 Sun의 JVM은 JDK 1.0·1.1의 Classic VM이었고, &lt;a href=&quot;https://www.oracle.com/java/technologies/whitepaper.html&quot;&gt;The Java HotSpot Performance Engine Architecture&lt;/a&gt;의 &apos;Handleless Objects&apos; 절은 이 Classic VM이 간접 핸들을 썼다고 설명합니다. 같은 절에 따르면 현재 Oracle JDK와 OpenJDK가 공통으로 쓰는 HotSpot은 핸들을 쓰지 않고 객체 참조를 객체를 직접 가리키는 포인터로 구현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드 호출 시의 전달 방식은 &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se26/html/jls-8.html#jls-8.4.1&quot;&gt;8.4.1. Formal Parameters&lt;/a&gt; 절에 있습니다.
인자 표현식의 &apos;값&amp;#8217;이 새로 만들어진 파라미터 변수를 초기화한다고 설명할 뿐입니다. 앞에서 정의한 call by reference처럼 파라미터가 호출한 쪽 변수의 별칭이 된다는 서술은 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When the method or constructor is invoked (§15.12), the values of the actual argument expressions initialize newly created parameter variables, each of the declared type, before execution of the body of the method or constructor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드나 생성자를 호출하면(§15.12), 그 본문을 실행하기 전에 실제 인자 표현식의 값이 새로 만들어진 파라미터 변수를 초기화한다. 새로 만들어지는 파라미터 변수는 각각 선언된 타입을 따른다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Java Language Specification&lt;br&gt;
&lt;cite&gt;8.4.1. Formal Parameters&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;메서드_호출마다_새로_만들어지는_프레임&quot;&gt;메서드 호출마다 새로 만들어지는 프레임&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JLS는 메서드 호출을 실행하는 과정도 단계별로 서술합니다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se26/html/jls-15.html#jls-15.12.4.5&quot;&gt;15.12.4.5. Create Frame, Synchronize, Transfer Control&lt;/a&gt; 절은 호출할 메서드가 정해지면 activation frame을 새로 만들고 그 안에 인자 값을 담는다고 설명합니다.
Activation frame은 메서드 호출의 파라미터, 지역 변수, 반환 주소 같은 실행 상태를 담고 호출이 끝나면 사라지는 메모리 영역입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now a new activation frame is created, containing the target reference (if any) and the argument values (if any), as well as enough space for the local variables and stack for the method to be invoked &amp;#8230;&amp;#8203; The effect of this is to assign the argument values to corresponding freshly created parameter variables of the method, and to make the target reference available as this, if there is a target reference.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이제 새 activation frame을 만든다. 여기에는 대상 참조(있다면)와 인자 값(있다면), 그리고 호출할 메서드의 지역 변수와 스택에 필요한 공간이 들어간다. &amp;#8230;&amp;#8203; 그 결과 인자 값은 메서드에 새로 만들어진 각각의 파라미터 변수에 대입되고, 대상 참조가 있다면 this로 쓸 수 있게 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Java Language Specification&lt;br&gt;
&lt;cite&gt;15.12.4.5. Create Frame, Synchronize, Transfer Control&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;호출한 쪽의 변수가 아니라 새 프레임에 새로 만들어진 파라미터 변수에 인자의 &apos;값&amp;#8217;을 대입한다는 서술입니다. 8.4.1절과 같은 결론입니다. 인스턴스 메서드를 호출한 대상 객체의 참조값(target reference)도 같은 방식으로 새 프레임에 담겨 &lt;code&gt;this&lt;/code&gt;가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 프레임을 가상 머신 수준에서 정의한 문서는 JVMS입니다. &lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-2.html#jvms-2.6&quot;&gt;2.6. Frames&lt;/a&gt; 절은 메서드를 호출할 때마다 새 프레임이 스레드의 JVM stack에 만들어지고, 프레임마다 자기만의 지역 변수 배열이 있다고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A new frame is created each time a method is invoked. &amp;#8230;&amp;#8203; Frames are allocated from the Java Virtual Machine stack (§2.5.2) of the thread creating the frame. Each frame has its own array of local variables (§2.6.1), &amp;#8230;&amp;#8203;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드를 호출할 때마다 새 프레임이 만들어진다. &amp;#8230;&amp;#8203; 프레임은 그 프레임을 만드는 스레드의 Java Virtual Machine stack(§2.5.2)에 할당된다. 각 프레임은 자기만의 지역 변수 배열(§2.6.1)을 가진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Java Virtual Machine Specification&lt;br&gt;
&lt;cite&gt;2.6. Frames&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파라미터는 이 지역 변수 배열로 전달됩니다. &lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-2.html#jvms-2.6.1&quot;&gt;2.6.1. Local Variables&lt;/a&gt; 절의 설명입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Java Virtual Machine uses local variables to pass parameters on method invocation. On class method invocation, any parameters are passed in consecutive local variables starting from local variable 0. On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language). Any parameters are subsequently passed in consecutive local variables starting from local variable 1.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java Virtual Machine은 메서드를 호출할 때 지역 변수로 파라미터를 전달한다. 클래스 메서드를 호출하면 파라미터는 0번 지역 변수부터 차례로 전달된다. 인스턴스 메서드를 호출하면 0번 지역 변수는 항상 그 인스턴스 메서드가 호출된 객체의 참조(Java 프로그래밍 언어의 this)를 전달하는 데 쓰인다. 파라미터는 그다음 1번 지역 변수부터 차례로 전달된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Java Virtual Machine Specification&lt;br&gt;
&lt;cite&gt;2.6.1. Local Variables&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 &lt;code&gt;reassign&lt;/code&gt; 예제에서 &lt;code&gt;myTv&lt;/code&gt;의 참조값이 파라미터 &lt;code&gt;targetTv&lt;/code&gt;로 복사되는 과정을 이 구조에 맞춰 따라가 보겠습니다. 그러려면 먼저 다음 개념을 알아야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Operand stack: 프레임마다 하나씩 있는 후입선출 스택으로, JVM 명령이 피연산자를 꺼내고 결과를 올리는 작업 공간입니다. 메서드에 넘길 인자를 준비하는 데도 쓰입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;바이트코드: javac가 Java 소스를 컴파일해 class 파일에 담는 JVM 명령어입니다. 이 예제의 호출 과정에는 두 명령이 등장합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-6.html#jvms-6.5.aload_n&quot;&gt;&lt;code&gt;aload_1&lt;/code&gt;&lt;/a&gt;: 현재 프레임의 1번 지역 변수에 담긴 참조값을 operand stack에 올립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-6.html#jvms-6.5.invokestatic&quot;&gt;&lt;code&gt;invokestatic&lt;/code&gt;&lt;/a&gt;: static 메서드를 호출합니다. 인자 값을 operand stack에서 꺼내고, 새 프레임을 만들어 그 값을 새 프레임의 0번 지역 변수부터 차례로 넣습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 개념으로 &lt;code&gt;reassign&lt;/code&gt; 호출을 그리면 다음 그림과 같습니다. &lt;code&gt;reassign&lt;/code&gt;은 static 메서드이므로 파라미터 &lt;code&gt;targetTv&lt;/code&gt;는 &lt;code&gt;reassign&lt;/code&gt; 프레임의 0번 지역 변수입니다. javac가 &lt;code&gt;main&lt;/code&gt; 프레임의 1번 지역 변수에 배정한 &lt;code&gt;myTv&lt;/code&gt;에 담긴 참조값이 이 칸으로 복사됩니다. 파라미터가 0번부터 들어가는 것은 JVMS가 정하지만, 파라미터가 아닌 지역 변수를 몇 번 칸에 둘지는 컴파일러가 정합니다. 바이트코드 수준에서는 &lt;code&gt;aload_1&lt;/code&gt; 명령이 이 참조값을 &lt;code&gt;main&lt;/code&gt; 프레임의 operand stack에 올리고, &lt;code&gt;invokestatic&lt;/code&gt; 명령이 그 값을 꺼내 새 프레임의 0번 지역 변수에 넣습니다. 두 칸은 서로 다른 프레임에 속한 별개의 저장 공간이므로 &lt;code&gt;targetTv&lt;/code&gt;에 새 객체를 대입해도 &lt;code&gt;main&lt;/code&gt; 프레임의 &lt;code&gt;myTv&lt;/code&gt;는 여전히 처음의 객체를 가리킵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/call-by-value/java-stack-frames.png&quot; alt=&quot;reassign 메서드의 프레임과 main 메서드의 프레임이 각자의 지역 변수 배열을 가지고 참조값만 복사되는 구조&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 JVMS는 추상 기계를 정의할 뿐입니다. 특정 구현의 메모리 구조가 아니라 명령의 동작 규칙을 정한 모델이라는 뜻입니다. JVMS 2장의 서두는 런타임 데이터 영역의 메모리 배치를 구현자의 재량에 맡긴다고 밝힙니다. 예를 들어 JIT 컴파일러가 만든 기계어 코드는 파라미터를 CPU 레지스터에 둘 수도 있습니다. 그렇더라도 호출한 쪽의 변수와 파라미터가 별개의 변수라는 언어의 의미는 달라지지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;kotlin&quot;&gt;Kotlin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kotlin도 call by reference를 지원하지 않습니다. 그런데 이 글의 판별 기준인 &apos;파라미터에 새 값을 대입했을 때 호출한 쪽의 변수가 바뀌는가&amp;#8217;를 Kotlin에서는 실험할 수 없습니다. &lt;a href=&quot;https://kotlinlang.org/spec/declarations.html#function-declaration&quot;&gt;Kotlin Language Specification&lt;/a&gt;의 Function declaration 절은 각 파라미터가 함수 본문 안에서 값의 이름으로 도입되므로 파라미터는 final이고 함수 안에서 바꿀 수 없다고 규정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Each parameter p&lt;sub&gt;i&lt;/sub&gt;: P&lt;sub&gt;i&lt;/sub&gt; = v&lt;sub&gt;i&lt;/sub&gt; introduces p&lt;sub&gt;i&lt;/sub&gt; as a name of value with type P&lt;sub&gt;i&lt;/sub&gt; available inside function body b; therefore, parameters are final and cannot be changed inside the function.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 파라미터 p&lt;sub&gt;i&lt;/sub&gt;: P&lt;sub&gt;i&lt;/sub&gt; = v&lt;sub&gt;i&lt;/sub&gt;는 함수 본문 b 안에서 쓸 수 있는 타입 P&lt;sub&gt;i&lt;/sub&gt;의 값 이름 p&lt;sub&gt;i&lt;/sub&gt;를 도입한다. 따라서 파라미터는 final이고 함수 안에서 바꿀 수 없다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Kotlin Language Specification&lt;br&gt;
&lt;cite&gt;Function declaration&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파라미터에 새 값을 대입하는 코드는 컴파일 오류입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Kotlin 파라미터 재할당과 객체 상태 변경&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;kotlin&quot;&gt;class Tv(var channel: String)

fun main() {
    val myTv = Tv(&quot;Classic&quot;)
    changeState(myTv)
    println(myTv.channel) // Rock
}

fun reassign(targetTv: Tv) {
    targetTv = Tv(&quot;Rock&quot;) // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
}

fun changeState(targetTv: Tv) {
    targetTv.channel = &quot;Rock&quot; // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;컴파일 오류가 납니다. 오류 메시지는 &quot;&apos;val&apos; cannot be reassigned&quot;입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파라미터가 가리키는 객체의 속성을 바꾸므로 호출한 쪽에서도 바뀐 값이 보입니다. 컴파일 오류가 나는 &lt;code&gt;reassign&lt;/code&gt; 함수를 지우고 실행하면 &lt;code&gt;Rock&lt;/code&gt;이 출력됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;재할당이 막혀 있을 뿐 나머지는 Java와 같습니다. JVM에서 실행할 때 전달되는 것은 Java와 같은 참조값의 복사본입니다. 파라미터가 final이라는 규칙은 언어 사양이므로 Kotlin/JS나 Kotlin/Native 등 JVM이 아닌 플랫폼으로 컴파일해도 달라지지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;javascript&quot;&gt;JavaScript&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JavaScript도 Java와 같은 방식으로 동작합니다. 먼저 swap 예제입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;JavaScript swap 함수&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;function swap(x, y) {
  const temp = x;
  x = y;
  y = temp;
}

let x1 = &quot;Hello&quot;;
let y1 = &quot;World&quot;;
swap(x1, y1);
console.log(x1); // Hello&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;객체를 전달할 때 함수 안에서 재할당하거나 상태를 바꾼 결과도 Java와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;JavaScript 객체 전달 시 재할당과 상태 변경&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;const myTv = { channel: &quot;Classic&quot; };

reassign(myTv);
console.log(myTv.channel); // Classic

changeState(myTv);
console.log(myTv.channel); // Rock

function reassign(targetTv) {
  targetTv = { channel: &quot;Rock&quot; }; // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
}

function changeState(targetTv) {
  targetTv.channel = &quot;Rock&quot;; // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;파라미터 &lt;code&gt;targetTv&lt;/code&gt;에 새 객체를 대입해도 호출한 쪽의 &lt;code&gt;myTv&lt;/code&gt;는 바뀌지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파라미터가 가리키는 객체의 프로퍼티를 바꾸면 호출한 쪽의 &lt;code&gt;myTv&lt;/code&gt;에서도 바뀐 값이 보입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 두 그림에서 &lt;code&gt;Tv&lt;/code&gt; 객체를 객체 리터럴로 바꾸면 JavaScript의 동작을 설명하는 그림이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions#passing_arguments&quot;&gt;MDN의 Functions 문서&lt;/a&gt;도 이 동작을 같은 구조로 정리합니다. 인자는 항상 값으로 전달되며, 객체 인자는 더 정확히 말하면 공유로 전달된다는 설명입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Arguments are always passed by value and never passed by reference. This means that if a function reassigns a parameter, the value won&amp;#8217;t change outside the function. More precisely, object arguments are passed by sharing, which means if the object&amp;#8217;s properties are mutated, the change will impact the outside of the function.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인자는 항상 값으로 전달되며 참조로 전달되는 일은 없다. 즉 함수가 파라미터를 재할당해도 함수 밖의 값은 바뀌지 않는다. 더 정확히 말하면 객체 인자는 공유로 전달되므로, 객체의 프로퍼티를 바꾸면 그 변경이 함수 밖에 영향을 준다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; MDN Web Docs&lt;br&gt;
&lt;cite&gt;Functions - Passing arguments&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ECMAScript 사양도 이 동작을 호출자 변수의 별칭이 아니라, 인자 값으로 새 파라미터 바인딩을 초기화하는 과정으로 기술합니다. &lt;a href=&quot;https://tc39.es/ecma262/2026/multipage/ecmascript-language-expressions.html#sec-argument-lists-runtime-semantics-argumentlistevaluation&quot;&gt;ArgumentListEvaluation&lt;/a&gt;은 인자 표현식에서 &lt;code&gt;GetValue&lt;/code&gt;로 값을 얻어 ECMAScript 언어 값 목록을 만들고, &lt;a href=&quot;https://tc39.es/ecma262/2026/multipage/ordinary-and-exotic-objects-behaviours.html#sec-functiondeclarationinstantiation&quot;&gt;FunctionDeclarationInstantiation&lt;/a&gt;은 그 목록으로 새 함수 실행 환경의 파라미터 바인딩을 초기화합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ECMAScript 사양에서는 Java와 달리 객체에 접근하는 값을 &apos;reference value&amp;#8217;라고 부르지 않습니다. 사양이 프로그램에서 직접 다루는 값의 종류로 정한 여덟 가지 언어 타입(ECMAScript language type) 중 하나가 Object이고, 객체 자체가 그 타입의 값입니다. 따라서 이 글의 &apos;참조값 복사&amp;#8217;는 관찰되는 동작을 설명하기 위한 모델로 이해하는 편이 정확합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;TypeScript도 인자 전달 방식은 JavaScript와 같습니다. &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html#runtime-behavior&quot;&gt;TypeScript 핸드북&lt;/a&gt;은 원칙적으로 TypeScript는 JavaScript 코드의 런타임 동작을 바꾸지 않는다고 밝힙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As a principle, TypeScript never changes the runtime behavior of JavaScript code.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원칙적으로 TypeScript는 JavaScript 코드의 런타임 동작을 절대 바꾸지 않는다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; TypeScript Handbook&lt;br&gt;
&lt;cite&gt;TypeScript for the New Programmer - Runtime Behavior&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;TypeScript 컴파일러는 타입 검사가 끝나면 타입 정보를 지우고 평범한 JavaScript를 내놓으므로 인자 전달 방식도 위에서 설명한 JavaScript와 동일합니다. 뒤에서 살펴볼 C#의 &lt;code&gt;ref&lt;/code&gt;, `out`에 해당하는 참조 파라미터 문법도 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;javascript_the_definitive_guide의_call_by_reference_서술&quot;&gt;JavaScript: The Definitive Guide의 call by reference 서술&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일부만 읽으면 JavaScript의 함수 호출을 call by reference로 오해하기 쉬운 자료도 있습니다. David Flanagan이 쓰고 오랫동안 JavaScript의 표준적인 책으로 꼽혀온 &lt;a href=&quot;https://docstore.mik.ua/orelly/webprog/jscript/ch11_02.htm&quot;&gt;&lt;strong&gt;JavaScript: The Definitive Guide&lt;/strong&gt; 4판&lt;/a&gt;은 11.2절 &apos;By Value Versus by Reference&amp;#8217;의 요약 표에서 객체가 by reference로 복사·전달·비교된다고 정리합니다. 이 요약 표만 보면 JavaScript는 call by reference라고 설명해도 될 것처럼 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 같은 절의 동작 설명을 읽어 보면, 이 책이 말하는 by reference는 이 글의 앞부분에서 정의한 call by reference와 다른 의미입니다. &apos;객체 값 전체가 복사되는 대신 객체를 가리키는 참조가 전달된다&amp;#8217;는 의미로 쓴 표현이고, 파라미터를 재할당해도 호출한 쪽 변수에 반영되지 않는다는 점은 이 책도 똑같이 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A function can use the reference to modify properties of the object or elements of the array. But if the function overwrites the reference with a reference to a new object or array, that modification is not visible outside of the function. Readers familiar with the other meaning of this term may prefer to say that objects and arrays are passed by value, but the value that is passed is actually a reference rather than the object itself.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;함수는 그 참조로 객체의 프로퍼티나 배열의 원소를 수정할 수 있다. 그러나 함수가 그 참조를 새 객체나 새 배열의 참조로 덮어쓰면, 그 변경은 함수 밖에서 보이지 않는다. 이 용어의 다른 뜻에 익숙한 독자라면 객체와 배열이 값으로 전달되며 다만 전달되는 값이 객체 자체가 아니라 참조라고 말하고 싶을 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; David Flanagan&lt;br&gt;
&lt;cite&gt;JavaScript: The Definitive Guide 4판, 11.2 By Value Versus by Reference&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 설명에 이어지는 예제의 제목도 &apos;References themselves are passed by value&apos;, 즉 참조 자체는 값으로 전달된다는 것입니다. 결국 관찰되는 동작을 설명하는 내용은 이 글과 같고, 전달되는 것을 무엇이라고 부르는지만 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;c&quot;&gt;C&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C에는 C&amp;#43;&amp;#43;의 참조 파라미터 같은 문법이 없으므로 call by value만 있습니다. C11 표준 초안 &lt;a href=&quot;https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf&quot;&gt;N1570&lt;/a&gt;의 6.5.2.2 Function calls 4항은 함수 호출을 준비할 때 인자를 평가하고 각 파라미터에 대응하는 인자의 값을 대입한다고 규정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In preparing for the call to a function, the arguments are evaluated, and each parameter is assigned the value of the corresponding argument.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;함수 호출을 준비할 때 인자를 평가하고, 각 파라미터에 대응하는 인자의 값을 대입한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; C11 Working Draft N1570&lt;br&gt;
&lt;cite&gt;6.5.2.2 Function calls 4항&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 항에 달린 각주 93은 함수가 파라미터의 값을 바꿀 수는 있지만 그 변경이 인자의 값에 영향을 줄 수는 없고, 대신 객체를 가리키는 포인터를 넘기면 함수가 그 객체의 값을 바꿀 수 있다고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;C 포인터 파라미터로 구현한 swap&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;void swap(int* x, int* y) {
    int temp = *x;
    *x = *y; // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
    *y = temp;
}

int a = 1;
int b = 2;
swap(&amp;amp;a, &amp;amp;b); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;포인터가 가리키는 변수에 값을 쓰므로 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;가 바뀝니다. 그러나 &lt;code&gt;x&lt;/code&gt;에 다른 주소를 대입해도 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;는 그대로입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;는 주소를 구하는 연산자일 뿐이고, 포인터라는 값의 복사본이 파라미터 &lt;code&gt;x&lt;/code&gt;와 &lt;code&gt;y&lt;/code&gt;에 대입됩니다. 호출 뒤 &lt;code&gt;a&lt;/code&gt;는 2, &lt;code&gt;b&lt;/code&gt;는 1입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 정리한 구분 기준을 적용하면 포인터 파라미터에 새 값을 대입해도 호출한 쪽 변수는 바뀌지 않으므로 call by value입니다. Java가 객체의 참조값을 전달하는 방식과 같은 구조입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;c_2&quot;&gt;C&amp;#43;&amp;#43;&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C&amp;#43;&amp;#43;는 이 글의 정의에 맞는 call by reference인 참조 파라미터를 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;C&amp;#43;&amp;#43; 참조 파라미터로 구현한 swap&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;cpp&quot;&gt;void swap(int&amp;amp; x, int&amp;amp; y) { // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
    int temp = x;
    x = y; // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
    y = temp;
}

int a = 1;
int b = 2;
swap(a, b); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;파라미터를 &lt;code&gt;int&amp;amp;&lt;/code&gt; 타입으로 선언하면 &lt;code&gt;x&lt;/code&gt;는 호출한 쪽 변수 &lt;code&gt;a&lt;/code&gt;의 별칭이 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;함수 안의 대입이 호출한 쪽 변수를 실제로 바꿉니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C의 &lt;code&gt;swap(&amp;amp;a, &amp;amp;b)&lt;/code&gt;와 달리 호출 지점에 주소를 넘긴다는 표시가 없습니다. 호출 뒤 &lt;code&gt;a&lt;/code&gt;는 2, &lt;code&gt;b&lt;/code&gt;는 1입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;포인터_파라미터와_참조_파라미터의_차이&quot;&gt;포인터 파라미터와 참조 파라미터의 차이&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C&amp;#43;&amp;#43;에는 C에서 물려받은 포인터 파라미터(예: &lt;code&gt;int*&lt;/code&gt;)도 있어서 둘을 구분해야 합니다. 포인터 파라미터는 C&amp;#43;&amp;#43;에서도 call by value입니다. &lt;code&gt;void swap(int* x, int* y)&lt;/code&gt;처럼 포인터를 받으면 앞의 C 예제와 똑같이 포인터 값의 복사본이 파라미터에 대입됩니다. 함수 안에서 &lt;code&gt;x&lt;/code&gt;에 다른 주소를 대입해도 호출한 쪽 변수에는 반영되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반면 &lt;code&gt;int&amp;amp;&lt;/code&gt; 참조 파라미터에는 재할당할 문법 자체가 없습니다. &lt;code&gt;x = y;&lt;/code&gt;는 참조를 다른 변수에 다시 묶는 문장이 아니라, 참조가 가리키는 객체에 값을 대입하는 문장입니다. 앞에서 정리한 구분 기준을 두 경우에 적용하면 결과가 다릅니다. 포인터 파라미터(&lt;code&gt;int*&lt;/code&gt;)에 대입하면 호출한 쪽 변수는 그대로지만, 참조 파라미터(&lt;code&gt;int&amp;amp;&lt;/code&gt;)에 대입하면 호출한 쪽 변수가 바뀝니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;c_표준이_기술하는_참조&quot;&gt;C&amp;#43;&amp;#43; 표준이 기술하는 참조&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C&amp;#43;&amp;#43; 표준도 참조를 전달되는 값이 아니라 별칭으로 기술합니다. &lt;a href=&quot;https://timsong-cpp.github.io/cppwp/n4950/dcl.ref&quot;&gt;[dcl.ref]&lt;/a&gt;의 주석은 참조를 &quot;a name of an object&quot;, 즉 객체의 이름으로 생각할 수 있다고 설명합니다. 같은 절 4항은 참조가 저장 공간을 필요로 하는지는 규정하지 않는다고 명시합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It is unspecified whether or not a reference requires storage ([basic.stc]).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참조가 저장 공간을 필요로 하는지 여부는 규정하지 않는다([basic.stc]).&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; C&amp;#43;&amp;#43; Working Draft N4950&lt;br&gt;
&lt;cite&gt;[dcl.ref] 4항&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컴파일러가 대개 참조를 주소로 구현하더라도 표준은 참조에 저장 공간이 있는지조차 정하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기계 수준에서 무엇이 복사되는지를 기준으로 삼으면 call by reference인 언어는 하나도 없게 됩니다. 두 용어는 컴파일된 결과가 아니라 언어가 정의한 의미론을 구분하는 말입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 C&amp;#43;&amp;#43; 표준이 참조 파라미터를 call by reference라고 부르지는 않습니다. 표준 문서 전체를 찾아보면 &apos;call by value&apos;, &apos;call by reference&apos;, &apos;pass by value&amp;#8217;라는 용어는 한 번도 나오지 않고 &apos;passed by reference&amp;#8217;만 네 번 나오는데, 모두 규범적인 정의가 아니라 주석이나 라이브러리 절의 설명입니다. 표준은 평가 전략에 이름을 붙이는 대신 동작으로 규정합니다. &lt;a href=&quot;https://timsong-cpp.github.io/cppwp/n4950/expr.call&quot;&gt;[expr.call]&lt;/a&gt; 6항은 각 파라미터가 대응하는 인자로 초기화된다고만 적습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When a function is called, each parameter ([dcl.fct]) is initialized ([dcl.init], [class.copy.ctor]) with its corresponding argument.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;함수가 호출되면 각 파라미터([dcl.fct])는 대응하는 인자로 초기화된다([dcl.init], [class.copy.ctor]).&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; C&amp;#43;&amp;#43; Working Draft N4950&lt;br&gt;
&lt;cite&gt;[expr.call] 6항&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참조 타입 파라미터가 어떻게 초기화되는지는 참조 바인딩 규칙이 따로 정합니다. Call by value와 call by reference는 특정 언어의 표준 용어가 아니라 여러 언어의 인자 전달 방식을 비교하기 위한 프로그래밍 언어론의 용어입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;c_3&quot;&gt;C#&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C#도 C&amp;#43;&amp;#43;처럼 call by reference를 문법으로 지원합니다. &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/method-parameters&quot;&gt;C# 레퍼런스의 Method parameters 문서&lt;/a&gt;는 C#이 기본적으로 인자를 값으로 전달한다고 밝히고, struct 같은 값 타입은 값의 복사본을, class 같은 참조 타입은 참조의 복사본을 메서드가 받는다고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By default, C# passes arguments to functions by value. This approach passes a copy of the variable to the method. For value (struct) types, the method gets a copy of the value. For reference (class) types, the method gets a copy of the reference.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기본적으로 C#은 인자를 함수에 값으로 전달한다. 이 방식은 변수의 복사본을 메서드에 넘긴다. 값 타입(struct)이면 메서드는 값의 복사본을 받고, 참조 타입(class)이면 참조의 복사본을 받는다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; C# reference&lt;br&gt;
&lt;cite&gt;Method parameters and modifiers&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참조 타입을 값으로 넘기면 메서드 안에서 파라미터를 재할당해도 호출한 쪽의 변수는 바뀌지 않고, 인스턴스 멤버를 바꾸면 두 변수가 같은 인스턴스를 가리키므로 호출한 쪽 변수로 접근해도 바뀌어 있다는 설명도 이어집니다. 여기까지는 Java와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파라미터를 참조로 전달하려면 &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;ref readonly&lt;/code&gt; 수정자를 붙입니다. 이 가운데 호출한 쪽 변수에 대입까지 할 수 있는 것은 `ref`와 `out`입니다. 같은 문서는 참조로 전달된 파라미터가 자기 값을 갖지 않고 다른 변수(referent)를 가리키는 참조 변수(reference variable)라고 설명합니다. 이 글의 정의대로 파라미터가 호출자 변수의 별칭이 되는 call by reference입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;C# ref 파라미터로 구현한 Swap&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;csharp&quot;&gt;void Swap(ref int x, ref int y) // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
{
    int temp = x;
    x = y; // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
    y = temp;
}

int a = 1;
int b = 2;
Swap(ref a, ref b); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt; 수정자를 붙인 파라미터 &lt;code&gt;x&lt;/code&gt;는 호출한 쪽 변수 &lt;code&gt;a&lt;/code&gt;의 별칭이 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파라미터에 대입하면 호출한 쪽 변수가 바뀝니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C&amp;#43;&amp;#43;와 달리 호출 지점에도 `ref`를 써야 합니다. 호출 뒤 &lt;code&gt;a&lt;/code&gt;는 2, &lt;code&gt;b&lt;/code&gt;는 1입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt; 파라미터는 메서드 선언과 호출 지점 양쪽에 `ref`를 명시해야 하므로, C&amp;#43;&amp;#43;의 참조 파라미터와 달리 호출식만 보고도 호출한 쪽 변수가 바뀔 수 있음을 알 수 있습니다. `out`은 호출한 쪽에서 초기화하지 않은 변수를 넘기고 메서드가 반드시 값을 대입해야 하는 변형이고, `in`과 `ref readonly`는 메서드가 값을 바꿀 수 없는 읽기 전용 참조입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`ref`는 참조 타입 변수에도 쓸 수 있습니다. class 인스턴스를 그냥 넘기면 Java처럼 참조의 복사본이 전달되어 파라미터 재할당이 호출자 변수에 반영되지 않지만, `ref`로 넘기면 파라미터가 호출자 변수의 별칭이 되므로 재할당으로 호출자 변수가 가리키는 객체 자체를 바꿀 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;C# ref 파라미터로 호출자의 참조 타입 변수 교체&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;csharp&quot;&gt;var myTv = new Tv();
myTv.Channel = &quot;Classic&quot;;

Replace(ref myTv); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
Console.WriteLine(myTv.Channel); // Rock

void Replace(ref Tv targetTv)
{
    targetTv = new Tv(); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
    targetTv.Channel = &quot;Rock&quot;;
}

class Tv
{
    public string Channel { get; set; } = &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Replace(Tv targetTv)&lt;/code&gt;처럼 &lt;code&gt;ref&lt;/code&gt; 없이 선언하고 &lt;code&gt;Replace(myTv)&lt;/code&gt;로 호출하면 Java의 &lt;code&gt;reassign(myTv)&lt;/code&gt;와 같아서 &lt;code&gt;Classic&lt;/code&gt;이 출력됩니다. 선언에 `ref`가 있는 상태에서 호출 지점의 `ref`만 빼면 컴파일 오류입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ref&lt;/code&gt; 파라미터에 새 객체를 대입하면 호출한 쪽의 &lt;code&gt;myTv&lt;/code&gt;가 새 객체를 가리키게 됩니다. .NET 8로 실행하면 &lt;code&gt;Rock&lt;/code&gt;이 출력됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;참조 타입을 값으로 전달&amp;#8217;하는 것과 &apos;변수를 ref로 참조 전달&amp;#8217;하는 것이 서로 다른 동작이라는 점을 C#은 문법으로 구분합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;python&quot;&gt;Python&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Python에도 Java처럼 call by value만 있습니다. 공식 튜토리얼 &lt;a href=&quot;https://docs.python.org/3/tutorial/controlflow.html#defining-functions&quot;&gt;4.8 Defining Functions&lt;/a&gt;는 인자가 호출된 함수의 지역 심벌 테이블에 도입되므로 값이 항상 객체 참조인 call by value로 전달된다고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;함수 호출의 실제 파라미터(인자)는 호출 시점에 호출된 함수의 지역 심벌 테이블에 도입된다. 따라서 인자는 call by value로 전달된다(이때 값은 항상 객체 참조이지, 객체의 값이 아니다).&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; The Python Tutorial&lt;br&gt;
&lt;cite&gt;4.8 Defining Functions&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 문장에 달린 각주는 변경 가능한 객체를 넘기면 호출한 쪽에서도 변경이 보이므로 call by object reference가 더 나은 표현이라고 덧붙입니다. &lt;a href=&quot;https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference&quot;&gt;Python FAQ&lt;/a&gt;는 더 직접적으로 답합니다. 인자는 대입으로 전달되고(passed by assignment), 대입은 객체에 대한 참조를 만들 뿐이므로 호출한 쪽과 호출된 쪽의 인자 이름 사이에 별칭이 없고, 따라서 call by reference도 없다는 설명입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Python swap 함수와 객체 전달&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;def swap(x, y):
    x, y = y, x  # &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;

a, b = 1, 2
swap(a, b)
print(a)  # 1


class Tv:
    def __init__(self, channel):
        self.channel = channel


def main():
    my_tv = Tv(&quot;Classic&quot;)
    reassign(my_tv)
    print(my_tv.channel)  # Classic
    changeState(my_tv)
    print(my_tv.channel)  # Rock


def reassign(targetTv):
    targetTv = Tv(&quot;Rock&quot;)  # &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;


def changeState(targetTv):
    targetTv.channel = &quot;Rock&quot;  # &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;


main()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;지역 이름 &lt;code&gt;x&lt;/code&gt;와 &lt;code&gt;y&lt;/code&gt;를 서로 다른 객체에 다시 묶을 뿐이라 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;는 1로 남습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파라미터 &lt;code&gt;targetTv&lt;/code&gt;를 새 객체에 다시 묶으므로 호출한 쪽의 &lt;code&gt;my_tv&lt;/code&gt;는 그대로이고, &lt;code&gt;Classic&lt;/code&gt;이 출력됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;targetTv&lt;/code&gt;가 가리키는 객체의 속성을 바꾸므로 호출한 쪽의 &lt;code&gt;my_tv&lt;/code&gt;로 접근해도 바뀌어 있고, &lt;code&gt;Rock&lt;/code&gt;이 출력됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java의 &lt;code&gt;Tv&lt;/code&gt; 예제, JavaScript의 &lt;code&gt;reassign&lt;/code&gt;과 &lt;code&gt;changeState&lt;/code&gt; 예제와 동일한 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정수나 문자열을 넘기면 호출한 쪽 변수가 그대로인데 리스트나 딕셔너리를 넘기면 바뀌어 있어서, 타입에 따라 전달 규칙이 다르다고 오해하기 쉽습니다. 전달 규칙은 같습니다. 정수와 문자열은 불변 객체라 내부 상태를 바꿀 방법이 없을 뿐입니다. 리스트를 넘겨도 함수 안에서 파라미터를 &lt;code&gt;x = [1]&lt;/code&gt;처럼 다시 묶으면 호출한 쪽은 그대로입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;go&quot;&gt;Go&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Go도 함수 호출에서 호출자 변수 자체를 파라미터의 암묵적인 별칭으로 만들지 않습니다. &lt;a href=&quot;https://go.dev/ref/spec#Calls&quot;&gt;Go 사양&lt;/a&gt;의 Calls 절은 함수 값과 인자를 평가한 뒤 파라미터와 결과를 포함한 함수의 변수를 위해 새 저장 공간을 할당하고, 인자를 대응하는 파라미터에 대입한다고 설명합니다. &lt;a href=&quot;https://go.dev/doc/faq#pass_by_value&quot;&gt;Go FAQ&lt;/a&gt;는 더 직접적으로 답합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As in all languages in the C family, everything in Go is passed by value. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;C 계열의 모든 언어처럼 Go에서도 모든 것이 값으로 전달된다. 즉 함수는 넘긴 것의 복사본을 항상 받으며, 대입문으로 파라미터에 값을 대입한 것과 같다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Go FAQ&lt;br&gt;
&lt;cite&gt;When are function parameters passed by value?&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;C 계열의 모든 언어&amp;#8217;를 글자 그대로 읽으면 맞지 않습니다. 이 글에서 살펴본 C&amp;#43;&amp;#43;과 C#은 C 계열이지만 참조 파라미터를 문법으로 지원합니다. 다만 두 언어도 기본 전달 방식은 값 전달이고 참조 전달은 수정자를 붙여야 하는 예외이므로, 이 문장은 &apos;C 계열 언어의 기본은 값 전달이고 Go에는 그 기본만 있다&amp;#8217;는 뜻으로 읽는 것이 알맞습니다. Go는 C처럼 호출자 변수를 바꾸려면 포인터를 명시적으로 넘겨야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Go 포인터 파라미터로 구현한 swap&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;func swap(x, y *int) {
    *x, *y = *y, *x // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
}

a, b := 1, 2
swap(&amp;amp;a, &amp;amp;b) // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;포인터가 가리키는 변수에 값을 쓰므로 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;와 &lt;code&gt;b&lt;/code&gt;가 바뀝니다. 함수 안에서 &lt;code&gt;x&lt;/code&gt;에 다른 포인터를 대입해도 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;는 바뀌지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;일반 함수로 호출한 쪽의 값을 바꾸려면 C처럼 포인터를 넘깁니다. 포인터 값의 복사본이 파라미터 &lt;code&gt;x&lt;/code&gt;와 &lt;code&gt;y&lt;/code&gt;에 대입됩니다. 호출 뒤 &lt;code&gt;a&lt;/code&gt;는 2, &lt;code&gt;b&lt;/code&gt;는 1입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 메서드 호출에서는 호출 지점에 &lt;code&gt;&amp;amp;&lt;/code&gt;가 없어도 됩니다. &lt;a href=&quot;https://go.dev/ref/spec#Calls&quot;&gt;Go 사양의 Calls 절&lt;/a&gt;은 &lt;code&gt;x&lt;/code&gt;가 주소를 구할 수 있는 값이고 &lt;code&gt;&amp;amp;x&lt;/code&gt;의 메서드 집합에 &lt;code&gt;m&lt;/code&gt;이 있으면 &lt;code&gt;x.m()&lt;/code&gt;이 &lt;code&gt;(&amp;amp;x).m()&lt;/code&gt;의 축약 표기라고 규정합니다. 포인터 수신자 메서드는 이 규칙에 따라 컴파일러가 주소를 대신 구해 넘기므로, 호출식의 모양만으로 호출한 쪽 변수가 바뀌는지 판단할 수는 없습니다. 이때도 전달되는 것은 포인터 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;rust&quot;&gt;Rust&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rust는 이 글에서 정의한 의미의 call by reference, 즉 파라미터가 호출자 변수의 별칭이 되는 전달 방식을 지원하지 않습니다. &lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/scope/borrow.html&quot;&gt;Rust by Example&lt;/a&gt;은 &lt;code&gt;&amp;amp;T&lt;/code&gt;를 넘기는 것을 &apos;passed by reference&amp;#8217;라고 부릅니다. 그러나 함수 호출이 호출자 변수의 별칭을 만들지는 않으며, &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 같은 참조도 하나의 값으로서 call by value로 전달됩니다. &lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-and-functions&quot;&gt;Rust Book&lt;/a&gt;은 변수를 함수에 넘기면 대입과 마찬가지로 값이 이동하거나 복사된다고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Passing a variable to a function will move or copy, just as assignment does.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;변수를 함수에 넘기면 대입과 마찬가지로 이동하거나 복사된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; The Rust Programming Language&lt;br&gt;
&lt;cite&gt;4.1 What is Ownership? - Ownership and Functions&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 설명에서 &apos;이동&amp;#8217;을 이해하려면 Rust의 소유권(ownership)을 알아야 합니다. &lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-rules&quot;&gt;Rust Book의 Ownership Rules 절&lt;/a&gt;은 소유권을 세 규칙으로 정리합니다. 모든 값에는 소유자(owner)인 변수가 있고, 소유자는 한 번에 하나뿐이며, 소유자가 스코프를 벗어나면 값이 해제됩니다. &lt;code&gt;let s2 = s1;&lt;/code&gt;처럼 대입하면 소유권이 &lt;code&gt;s2&lt;/code&gt;로 넘어가는데 이를 이동(move)이라 부르고, 이동한 뒤에 &lt;code&gt;s1&lt;/code&gt;을 쓰면 컴파일 오류가 납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;복사&amp;#8217;가 일어나는 타입은 &lt;code&gt;Copy&lt;/code&gt; 트레이트가 정합니다.
&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-02-traits.html&quot;&gt;트레이트&lt;/a&gt;는 여러 타입이 공유하는 동작을 선언하는 문법으로, 다른 언어의 인터페이스에 가깝습니다. 메서드 없이 컴파일러에게 타입의 성질만 알려 주는 트레이트도 있는데, &lt;code&gt;Copy&lt;/code&gt;가 그런 트레이트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Copy&lt;/code&gt;는 값을 다른 변수에 대입해도 원래 변수를 계속 쓸 수 있는 타입에 붙는 트레이트입니다. &lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#stack-only-data-copy&quot;&gt;Rust Book의 Stack-Only Data: Copy 절&lt;/a&gt;은 &lt;code&gt;Copy&lt;/code&gt;를 구현한 타입의 변수는 이동하지 않고 그대로 복사되어 다른 변수에 대입한 뒤에도 유효하다고 설명합니다. 정수, 실수, &lt;code&gt;bool&lt;/code&gt;, &lt;code&gt;char&lt;/code&gt;, 그리고 &lt;code&gt;Copy&lt;/code&gt;인 타입만 담은 튜플이 여기에 속합니다. 반면 &lt;code&gt;String&lt;/code&gt;이나 &lt;code&gt;Vec&lt;/code&gt;처럼 힙 메모리를 할당하거나 자원을 소유하는 타입은 &lt;code&gt;Copy&lt;/code&gt;가 될 수 없고, 값이 스코프를 벗어날 때 정리 작업을 하는 &lt;code&gt;Drop&lt;/code&gt;을 구현한 타입에 &lt;code&gt;Copy&lt;/code&gt;를 붙이면 컴파일 오류가 납니다. 메모리를 그대로 복제해도 안전한 타입만 &lt;code&gt;Copy&lt;/code&gt;가 되는 셈입니다. 그래서 &lt;code&gt;let t = s;&lt;/code&gt;에서 &lt;code&gt;s&lt;/code&gt;가 &lt;code&gt;i32&lt;/code&gt;이면 이후에도 &lt;code&gt;s&lt;/code&gt;를 쓸 수 있지만, &lt;code&gt;String&lt;/code&gt;이면 앞에서 본 것처럼 이동합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt;처럼 &lt;code&gt;Copy&lt;/code&gt;가 아닌 타입의 값은 소유권이 파라미터로 이동하고, &lt;code&gt;i32&lt;/code&gt;처럼 &lt;code&gt;Copy&lt;/code&gt;인 타입의 값은 복사됩니다. 다만 &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 타입의 변수를 &lt;code&gt;&amp;amp;mut T&lt;/code&gt; 파라미터에 넘기면 컴파일러가 암묵적으로 재대여(reborrow)하므로, &lt;code&gt;&amp;amp;mut T&lt;/code&gt;가 &lt;code&gt;Copy&lt;/code&gt;가 아니어도 호출 뒤에 그 변수를 다시 쓸 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글의 관점에서 이동과 복사는 모두 파라미터가 인자 값으로 초기화되는 같은 방식이고, 호출자 변수의 별칭이 생기지 않는다는 점도 같습니다. 다른 점은 호출 뒤에 호출자 변수를 계속 쓸 수 있느냐뿐입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Rust 가변 참조로 구현한 swap&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;fn swap(x: &amp;amp;mut i32, y: &amp;amp;mut i32) { // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
    let temp = *x;
    *x = *y; // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
    *y = temp;
}

let mut a = 1;
let mut b = 2;
swap(&amp;amp;mut a, &amp;amp;mut b); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;파라미터 &lt;code&gt;x&lt;/code&gt;에 전달되는 것은 &lt;code&gt;&amp;amp;mut i32&lt;/code&gt; 타입의 값입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;가변 참조가 가리키는 변수에 값을 쓰므로 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;가 바뀝니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;일반 함수로 호출한 쪽의 값을 바꾸려면 이렇게 가변 참조를 넘깁니다. 호출 뒤 &lt;code&gt;a&lt;/code&gt;는 2, &lt;code&gt;b&lt;/code&gt;는 1입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;표준 라이브러리의 &lt;a href=&quot;https://doc.rust-lang.org/std/mem/fn.swap.html&quot;&gt;&lt;code&gt;std::mem::swap&lt;/code&gt;&lt;/a&gt; 함수도 &lt;code&gt;fn swap&amp;lt;T&amp;gt;(x: &amp;amp;mut T, y: &amp;amp;mut T)&lt;/code&gt; 시그니처로 같은 방식을 씁니다. 가변 참조를 넘겨도 파라미터 자체를 재할당해서 호출자의 변수 바인딩을 바꾸는, 이 글에서 정의한 call by reference는 아닙니다. &lt;code&gt;x&lt;/code&gt;를 재할당하려면 파라미터를 &lt;code&gt;mut x: &amp;amp;mut i32&lt;/code&gt;로 선언해야 하고, 그렇게 재할당해도 호출한 쪽의 &lt;code&gt;a&lt;/code&gt;에는 아무 영향이 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드 호출에서는 Go의 포인터 수신자 메서드처럼 호출 지점에 &lt;code&gt;&amp;amp;mut&lt;/code&gt;가 없어도 됩니다. &lt;a href=&quot;https://doc.rust-lang.org/reference/expressions/method-call-expr.html#r-expr.method.autoref-deref&quot;&gt;Rust Reference의 메서드 호출 규칙&lt;/a&gt;에 따라 &lt;code&gt;v.push(1)&lt;/code&gt;처럼 쓰면 컴파일러가 수신자를 자동으로 대여해 &lt;code&gt;&amp;amp;mut v&lt;/code&gt;를 넘기며, 이때도 전달되는 것은 가변 참조 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ada_call_by_reference와_copy_restore의_차이&quot;&gt;Ada: call by reference와 copy-restore의 차이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정의 절에서 &apos;파라미터에 새 값을 대입했을 때 호출한 쪽의 변수가 바뀌는가&amp;#8217;라는 기준만으로는 모든 전달 방식을 분류할 수 없다고 했습니다. Ada가 그 예입니다. Ada는 파라미터에 &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;in out&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt; 모드를 선언하고, &lt;code&gt;in out&lt;/code&gt;이나 &lt;code&gt;out&lt;/code&gt; 파라미터에 대입한 값은 호출한 쪽 변수에 반영됩니다. 호출한 쪽에서 관찰되는 결과만 보면 C&amp;#43;&amp;#43;의 참조 파라미터와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 &lt;a href=&quot;https://ada-lang.io/docs/arm/AA-6/AA-6.2/&quot;&gt;Ada Reference Manual 6.2&lt;/a&gt;는 복사로 전달할지 참조로 전달할지를 모드가 아니라 주로 타입으로 정합니다. &lt;code&gt;Integer&lt;/code&gt; 같은 elementary type은 by-copy type이라 복사로 전달하고, tagged type 등 by-reference type은 참조로 전달하며, &lt;code&gt;aliased&lt;/code&gt;로 선언한 형식 파라미터는 타입과 관계없이 참조로 전달합니다. 어느 쪽에도 해당하지 않는 파라미터는 복사와 참조 중 어느 방식으로 전달할지 규정하지 않고 구현에 맡깁니다. 복사로 전달된 &lt;code&gt;in out&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt; 파라미터는 &lt;a href=&quot;https://ada-lang.io/docs/arm/AA-6/AA-6.4&quot;&gt;6.4.1&lt;/a&gt;에 따라 서브프로그램이 정상 종료할 때 그 값을 실제 인자에 되돌려 씁니다. 이것이 정의 절에서 언급한 call by copy-restore입니다. 호출한 쪽의 변수가 바뀐다는 결과는 call by reference와 같지만 실제 전달은 복사입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;복사 전달과 참조 전달의 차이는 프로시저 안에서 실제 인자로 넘긴 변수를 직접 읽어 보면 드러납니다. 아래 예제는 같은 변수 &lt;code&gt;Original&lt;/code&gt;을 by-copy로 받는 프로시저와 &lt;code&gt;aliased&lt;/code&gt;로 받는 프로시저에 각각 넘기고, 파라미터에 대입한 직후 &lt;code&gt;Original&lt;/code&gt;을 읽습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Ada by-copy 전달과 aliased 참조 전달의 차이&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;ada&quot;&gt;with Ada.Text_IO; use Ada.Text_IO;

procedure Demo is
   Original : aliased Integer := 1;

   procedure By_Copy (Param : in out Integer) is
   begin
      Param := 2;
      Put_Line (&quot;inside By_Copy: Original =&quot; &amp;amp; Integer&apos;Image (Original)); -- &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
   end By_Copy;

   procedure By_Reference (Param : aliased in out Integer) is
   begin
      Param := 2;
      Put_Line (&quot;inside By_Reference: Original =&quot; &amp;amp; Integer&apos;Image (Original)); -- &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
   end By_Reference;
begin
   By_Copy (Original);
   Put_Line (&quot;after By_Copy: Original =&quot; &amp;amp; Integer&apos;Image (Original)); -- &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;

   Original := 1;
   By_Reference (Original);
   Put_Line (&quot;after By_Reference: Original =&quot; &amp;amp; Integer&apos;Image (Original));
end Demo;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Integer&lt;/code&gt;는 by-copy 타입이므로 &lt;code&gt;Param&lt;/code&gt;은 &lt;code&gt;Original&lt;/code&gt;의 복사본입니다. &lt;code&gt;Param&lt;/code&gt;에 2를 대입한 직후에도 &lt;code&gt;Original&lt;/code&gt;은 아직 1입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로시저가 정상 종료하면서 &lt;code&gt;Param&lt;/code&gt;의 값을 &lt;code&gt;Original&lt;/code&gt;에 되돌려 써서 &lt;code&gt;Original&lt;/code&gt;이 2가 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;aliased&lt;/code&gt; 파라미터는 참조로 전달되므로 &lt;code&gt;Param&lt;/code&gt;은 &lt;code&gt;Original&lt;/code&gt;의 별칭입니다. &lt;code&gt;Param&lt;/code&gt;에 대입하는 순간 &lt;code&gt;Original&lt;/code&gt;도 2입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GCC에 포함된 Ada 컴파일러인 GNAT 13.3으로 실행한 출력입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;inside By_Copy: Original = 1
after By_Copy: Original = 2
inside By_Reference: Original = 2
after By_Reference: Original = 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;호출이 끝난 뒤의 값은 둘 다 2로 같지만, 프로시저 안에서 &lt;code&gt;Original&lt;/code&gt;을 읽은 결과는 다릅니다. 파라미터 모드를 문법으로 지원한다는 사실과 그 모드가 call by reference로 구현된다는 사실은 별개입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글에서 살펴본 언어의 인자 전달 방식을 표로 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 22.2222%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;언어&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Call by reference 지원&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;전달 방식&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;파라미터를 통해 호출한 쪽 변수를 바꾸는 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value. 객체는 참조값의 복사본 전달&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음. 객체 상태 변경만 공유&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Kotlin&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value. 파라미터가 final이라 재할당 자체가 컴파일 오류&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음. 객체 상태 변경만 공유&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JavaScript&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value. Object 값을 별도의 파라미터 바인딩에 대입&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음. 객체 상태 변경만 공유&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;C&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;포인터를 넘김(&lt;code&gt;swap(&amp;amp;a, &amp;amp;b)&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;C&amp;#43;&amp;#43;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;예. 참조 파라미터(&lt;code&gt;int&amp;amp;&lt;/code&gt;)에 한함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본은 call by value. 참조 파라미터만 call by reference&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;참조 파라미터 선언. C처럼 포인터를 넘길 수도 있음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;C#&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;예. &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;ref readonly&lt;/code&gt; 파라미터. 이 중 파라미터에 대입할 수 있는 것은 &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본은 call by value. class 인스턴스는 참조의 복사본 전달&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt; 파라미터 선언. 호출 지점에도 &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt; 표기&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Python&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value. 값은 항상 객체 참조&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음. 객체 상태 변경만 공유&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Go&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value. 맵·슬라이스 값은 포인터처럼 동작&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;포인터를 넘김(&lt;code&gt;swap(&amp;amp;a, &amp;amp;b)&lt;/code&gt;). 포인터 수신자 메서드는 &lt;code&gt;&amp;amp;&lt;/code&gt;를 자동으로 붙임&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Rust&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아니오&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;call by value. 대입과 같이 타입에 따라 이동 또는 복사하며, 가변 참조 &lt;code&gt;&amp;amp;mut T&lt;/code&gt;도 값으로 전달&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;가변 참조를 넘김(&lt;code&gt;swap(&amp;amp;mut a, &amp;amp;mut b)&lt;/code&gt;). 메서드 호출은 수신자를 자동으로 대여&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Ada&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;부분적으로. by-reference 타입이나 &lt;code&gt;aliased&lt;/code&gt;로 선언한 파라미터는 모드와 관계없이 참조로 전달. 그 밖의 타입은 명세가 정하지 않음. by-copy 타입의 &lt;code&gt;in out&lt;/code&gt;, `out`은 정상 종료 시 값을 되돌려 씀(copy-restore)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;타입에 따라 by-copy 또는 by-reference&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;in out&lt;/code&gt;, &lt;code&gt;out&lt;/code&gt; 모드 선언&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java, Kotlin, JavaScript, Python은 인자 값으로 별도의 파라미터 변수 또는 바인딩을 초기화합니다. 호출자 변수 자체가 파라미터의 별칭이 되는 call by reference를 지원하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;객체를 전달하면 객체가 복제되는 것도, 호출자의 변수 자체가 전달되는 것도 아닙니다. Java에서는 객체를 가리키는 참조값이, JavaScript에서는 Object 값이 별도의 파라미터에 전달되며 호출자와 피호출자가 같은 객체를 관찰할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;함수 안에서 파라미터를 재할당해도 호출한 쪽의 변수는 바뀌지 않습니다. 반면 파라미터가 가리키는 객체의 내부 상태를 바꾸면 호출한 쪽 변수로 접근해도 바뀌어 있습니다. 이 차이가 &quot;객체는 call by reference&quot;라는 오해의 근원입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C와 Go는 포인터를, Rust는 가변 참조를 넘겨서 호출한 쪽의 값을 바꿉니다. 변수의 주소나 참조를 그 자리에서 구해 넘기는 호출에서는 &lt;code&gt;&amp;amp;&lt;/code&gt;나 &lt;code&gt;&amp;amp;mut&lt;/code&gt;가 드러나지만, 이미 포인터나 참조를 담은 변수를 넘기거나 Go와 Rust의 메서드 호출처럼 컴파일러가 주소 취득과 대여를 대신하는 경우에는 호출식의 모양만으로 판단할 수 없습니다. 파라미터가 호출자 변수의 별칭이 되는 언어는 C&amp;#43;&amp;#43;, C#, Ada뿐이고, 그중 Ada는 타입에 따라 복사로 전달했다가 되돌려 쓰기도 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;객체를 공유하는 이 동작을 call by sharing이라고 부르기도 합니다. &lt;strong&gt;JavaScript: The Definitive Guide&lt;/strong&gt; 4판처럼 일부 자료는 객체를 공유하는 동작을 by reference라고 표현하지만, 호출자 변수의 별칭을 전달한다는 의미의 call by reference와는 구분해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;용어&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman, &lt;a href=&quot;https://www.pearson.com/en-us/subject-catalog/p/compilers-principles-techniques-and-tools/P200000003472/9780133002140&quot;&gt;&lt;strong&gt;Compilers: Principles, Techniques, and Tools&lt;/strong&gt;&lt;/a&gt; 2판, Addison-Wesley, 2007, 1.6.6 Parameter Passing Mechanisms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Christopher Strachey, &lt;a href=&quot;https://www.cs.cmu.edu/~crary/819-f09/Strachey67.pdf&quot;&gt;&lt;strong&gt;Fundamental Concepts in Programming Languages&lt;/strong&gt;&lt;/a&gt;, Higher-Order and Symbolic Computation 13권, 2000, 3.4.2 Parameter calling modes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.masswerk.at/algol60/report.htm&quot;&gt;Revised Report on the Algorithmic Language Algol 60 - 4.7.3 Semantics&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://csg.csail.mit.edu/pubs/memos/Memo-112/memo112-1a.pdf&quot;&gt;CLU 설계 문서 - Call by sharing&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se26/html/jls-8.html#jls-8.4.1&quot;&gt;Java Language Specification - 8.4.1. Formal Parameters&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se26/html/jls-4.html#jls-4.3.1&quot;&gt;Java Language Specification - 4.3.1. Objects&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se26/html/jls-15.html#jls-15.12.4.5&quot;&gt;Java Language Specification - 15.12.4.5. Create Frame, Synchronize, Transfer Control&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-2.html#jvms-2.6&quot;&gt;Java Virtual Machine Specification - 2.6. Frames&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-2.html#jvms-2.6.1&quot;&gt;Java Virtual Machine Specification - 2.6.1. Local Variables&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-2.html#jvms-2.7&quot;&gt;Java Virtual Machine Specification - 2.7. Representation of Objects&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se6/html/Overview.doc.html#16066&quot;&gt;Java Virtual Machine Specification (Java SE 6) - 3.7 Representation of Objects&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/20001213164500/http://java.sun.com/docs/books/vmspec/html/Overview.doc.html&quot;&gt;The Java Virtual Machine Specification 초판(1997) - 3.7 Representation of Objects (Wayback Machine)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.oracle.com/java/technologies/whitepaper.html&quot;&gt;The Java HotSpot Performance Engine Architecture - Handleless Objects&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/jvms-6.html#jvms-6.5.invokestatic&quot;&gt;Java Virtual Machine Specification - 6.5. invokestatic&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.java/learn/classes-objects/calling-methods-constructors/&quot;&gt;Dev.java - Calling Methods and Constructors&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ken Arnold, James Gosling, David Holmes, &lt;a href=&quot;https://www.informit.com/store/java-programming-language-9780321349804&quot;&gt;&lt;strong&gt;The Java Programming Language&lt;/strong&gt;&lt;/a&gt; 4판, Addison-Wesley, 2006&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kathy Sierra, Bert Bates, &lt;a href=&quot;https://www.oreilly.com/library/view/head-first-java/0596009208/&quot;&gt;&lt;strong&gt;Head First Java&lt;/strong&gt;&lt;/a&gt; 2판, O&amp;#8217;Reilly, 2005, 3장과 4장&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kotlin&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://kotlinlang.org/spec/declarations.html#function-declaration&quot;&gt;Kotlin Language Specification - Function declaration&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JavaScript&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://tc39.es/ecma262/2026/multipage/ecmascript-language-expressions.html#sec-argument-lists-runtime-semantics-argumentlistevaluation&quot;&gt;ECMAScript Language Specification - ArgumentListEvaluation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://tc39.es/ecma262/2026/multipage/ordinary-and-exotic-objects-behaviours.html#sec-functiondeclarationinstantiation&quot;&gt;ECMAScript Language Specification - FunctionDeclarationInstantiation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions#passing_arguments&quot;&gt;MDN Web Docs - Functions, Passing arguments&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html#runtime-behavior&quot;&gt;TypeScript Handbook - TypeScript for the New Programmer, Runtime Behavior&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;David Flanagan, &lt;a href=&quot;https://www.oreilly.com/library/view/javascript-the-definitive/0596000480/&quot;&gt;&lt;strong&gt;JavaScript: The Definitive Guide&lt;/strong&gt;&lt;/a&gt; 4판, O&amp;#8217;Reilly, 2001, &lt;a href=&quot;https://docstore.mik.ua/orelly/webprog/jscript/ch11_02.htm&quot;&gt;11.2 By Value Versus by Reference&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf&quot;&gt;C11 Working Draft N1570 - 6.5.2.2 Function calls&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C&amp;#43;&amp;#43;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://timsong-cpp.github.io/cppwp/n4950/dcl.ref&quot;&gt;C&amp;#43;&amp;#43; Working Draft N4950 - [dcl.ref] References&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://timsong-cpp.github.io/cppwp/n4950/expr.call&quot;&gt;C&amp;#43;&amp;#43; Working Draft N4950 - [expr.call] Function call&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;C#&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/method-parameters#reference-parameters&quot;&gt;C# Reference - Reference parameters&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Python&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/3/tutorial/controlflow.html#defining-functions&quot;&gt;The Python Tutorial - 4.8 Defining Functions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/3/faq/programming.html#how-do-i-write-a-function-with-output-parameters-call-by-reference&quot;&gt;Python Programming FAQ - How do I write a function with output parameters (call by reference)?&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/ref/spec#Calls&quot;&gt;The Go Programming Language Specification - Calls&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/faq#pass_by_value&quot;&gt;Go FAQ - When are function parameters passed by value?&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rust&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-rules&quot;&gt;The Rust Programming Language - Ownership Rules&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#ownership-and-functions&quot;&gt;The Rust Programming Language - Ownership and Functions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#stack-only-data-copy&quot;&gt;The Rust Programming Language - Stack-Only Data: Copy&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-02-traits.html&quot;&gt;The Rust Programming Language - Traits: Defining Shared Behavior&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/mem/fn.swap.html&quot;&gt;Rust Standard Library - std::mem::swap&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/reference/expressions/method-call-expr.html&quot;&gt;The Rust Reference - Method-call expressions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ada&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ada-lang.io/docs/arm/AA-6/AA-6.2/&quot;&gt;Ada Reference Manual - 6.2 Formal Parameter Modes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ada-lang.io/docs/arm/AA-6/AA-6.4&quot;&gt;Ada Reference Manual - 6.4 Subprogram Calls, 6.4.1 Parameter Associations&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java 문자열 더하기 연산의 최적화</title>
      <link>https://blog.benelog.net//java-string-concat.html</link>
      <pubDate>Sun, 23 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">java-string-concat.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#컴파일_타임_상수식의_결합&quot;&gt;컴파일 타임 상수식의 결합&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#변수가_섞인_결합의_stringbufferstringbuilder_치환&quot;&gt;변수가 섞인 결합의 StringBuffer·StringBuilder 치환&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#반복문_안에서_누적하는_문자열_결합&quot;&gt;반복문 안에서 누적하는 문자열 결합&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_9부터_적용된_invokedynamic_방식&quot;&gt;JDK 9부터 적용된 invokedynamic 방식&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#invokedynamic의_동작_구조&quot;&gt;invokedynamic의 동작 구조&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_25에서_확인한_invokedynamic_바이트코드&quot;&gt;JDK 25에서 확인한 invokedynamic 바이트코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#stringconcatfactory_결합_전략의_변화&quot;&gt;StringConcatFactory 결합 전략의 변화&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt;을 &lt;code&gt;+&lt;/code&gt; 연산자로 더하는 코드는 컴파일러와 JVM이 최적화합니다. JDK의 &lt;code&gt;javac&lt;/code&gt;은 JDK 1.4까지 &lt;code&gt;StringBuffer&lt;/code&gt;를 쓰는 코드로, JDK 5부터 JDK 8까지는 &lt;code&gt;StringBuilder&lt;/code&gt;를 쓰는 코드로 컴파일 시점에 변환했습니다. JDK 9부터는 기본적으로 &lt;code&gt;invokedynamic&lt;/code&gt; 명령으로 치환되고, 실제 결합 코드는 실행 시점에 링크합니다. 어떤 경우에 자동으로 최적화되는지 알고 있으면, 반복문 밖에서는 &lt;code&gt;+&lt;/code&gt; 연산을 그대로 쓰고 반복문 안에서 누적할 때만 &lt;code&gt;StringBuilder&lt;/code&gt;를 직접 쓰는 식으로 기준을 정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 언어 명세는 문자열 결합의 결과와 평가 순서를 규정하지만, 구체적인 구현 방식은 컴파일러의 재량에 맡깁니다. 따라서 이 글의 버전 구분은 JDK &lt;code&gt;javac&lt;/code&gt;의 기본 바이트코드와 OpenJDK 런타임 구현을 기준으로 합니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_1&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_1&quot; title=&quot;View footnote.&quot;&gt;1&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;컴파일_타임_상수식의_결합&quot;&gt;컴파일 타임 상수식의 결합&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문자열 타입의 컴파일 타임 상수식은 컴파일 시점에 평가되어 하나의 문자열이 되고, 이 문자열은 &lt;code&gt;String.intern()&lt;/code&gt;을 거친 것처럼 문자열 풀에 등록되어 공유됩니다. &lt;code&gt;String.intern()&lt;/code&gt;은 같은 내용에 대한 문자열 풀(string pool)의 canonical reference(대표 참조)를 반환합니다. 내용이 같은 &lt;code&gt;String&lt;/code&gt; 인스턴스가 힙에 여러 개 있어도 문자열 풀에는 내용별로 대표 인스턴스가 하나만 등록되는데, canonical reference는 그 대표 인스턴스를 가리키는 참조입니다. 그래서 내용이 같은 문자열을 intern하면 동일한 인스턴스를 참조하게 됩니다. 이는 힙에 존재하는 내용이 같은 모든 &lt;code&gt;String&lt;/code&gt; 객체가 자동으로 문자열 풀에 합쳐진다는 뜻은 아닙니다. 문자열 리터럴뿐 아니라 primitive 리터럴과 상수 변수(예: `static final int SIZE = 10;`으로 선언한 &lt;code&gt;SIZE&lt;/code&gt;)도 이런 상수식에 포함될 수 있습니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_2&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_2&quot; title=&quot;View footnote.&quot;&gt;2&lt;/a&gt;]&lt;/sup&gt; 예를 들어 아래의 두 선언은 같은 코드로 컴파일됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;String str1 = &quot;It&apos;s&quot; + &quot; a string&quot; + &quot;....&quot;;
String str2 = &quot;It&apos;s a string....&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 규칙은 이 글에서 다루는 모든 JDK 버전에서 같습니다. 뒤에서 소개할 JDK 25의 바이트코드에서도 두 변수가 상수 풀의 같은 항목(&lt;code&gt;ldc #7&lt;/code&gt;)을 읽는 것을 확인할 수 있습니다. 두 변수는 문자열 풀의 같은 인스턴스를 참조하므로 &lt;code&gt;equals()&lt;/code&gt; 메서드뿐 아니라 &lt;code&gt;str1 == str2&lt;/code&gt; 비교도 &lt;code&gt;true&lt;/code&gt;가 됩니다. 반면 비상수 결합식의 결과는 새 &lt;code&gt;String&lt;/code&gt;으로 만들어지고 자동으로 intern되지 않으므로, 내용이 같은 리터럴이나 별도로 만들어진 문자열과 &lt;code&gt;==&lt;/code&gt;로 비교하면 &lt;code&gt;false&lt;/code&gt;가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;변수가_섞인_결합의_stringbufferstringbuilder_치환&quot;&gt;변수가 섞인 결합의 StringBuffer·StringBuilder 치환&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;상수와 String 변수를 섞어서 더하는 연산을 JDK 8까지의 &lt;code&gt;javac&lt;/code&gt;으로 컴파일하면 &lt;code&gt;StringBuffer&lt;/code&gt;나 &lt;code&gt;StringBuilder&lt;/code&gt;의 &lt;code&gt;append()&lt;/code&gt;, &lt;code&gt;toString()&lt;/code&gt; 메서드를 호출하는 코드로 변환됩니다. JDK 1.4의 &lt;code&gt;javac&lt;/code&gt;도 &lt;code&gt;StringBuffer&lt;/code&gt;를 사용하는 방식이었습니다. 같은 시기의 Java ME 문서인 &lt;a href=&quot;https://docs.oracle.com/javame/config/cldc/ref-impl/cldc1.1/jsr139/java/lang/StringBuffer.html&quot;&gt;CLDC 1.1의 StringBuffer javadoc&lt;/a&gt;에는 당시 컴파일러의 변환 방식이 아래와 같이 설명되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;String buffers are used by the compiler to implement the binary string concatenation operator. For example, the code:
x = &quot;a&quot; + 4 + &quot;c&quot;
is compiled to the equivalent of:
x = new StringBuffer().append(&quot;a&quot;).append(4).append(&quot;c&quot;).toString()
which creates a new string buffer (initially empty), appends the string representation of each operand to the string buffer in turn, and then converts the contents of the string buffer to a string. Overall, this avoids creating many temporary strings.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 5부터 JDK 8까지의 &lt;code&gt;javac&lt;/code&gt;은 &lt;code&gt;StringBuffer&lt;/code&gt; 대신 &lt;code&gt;StringBuilder&lt;/code&gt;로 변환합니다. &lt;code&gt;StringBuilder&lt;/code&gt;의 메서드에는 `synchronized`가 없어서 `append()`를 호출할 때마다 락을 잡지 않습니다. 이를 바이트코드나 역컴파일 결과로 확인할 수 있습니다. 원래의 소스가 아래와 같을 때&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;String str0 = &quot;It&apos;s a string....&quot;;
String str1 = &quot;It&apos;s&quot; + &quot; a string&quot; + &quot;....&quot;;
String str2 = &quot;It&apos;s a string....&quot; + str0 + &quot;000&quot;;
str2 = str0 + str1 + &quot;1111&quot; ;
str2 = str2 + &quot;1111&quot;;
str2 += &quot;1111&quot;;
for (int i=0;i&amp;lt;10;i++){
    str2 = str2 + &quot;1111&quot;;
    str2 += &quot;1111&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 5 이상 JDK 8 이하의 &lt;code&gt;javac&lt;/code&gt;으로 컴파일한 후 역컴파일하면 다음과 같은 결과가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;String str0 = &quot;It&apos;s a string....&quot;;
String str1 = &quot;It&apos;s a string....&quot;;
String str2 = (new StringBuilder(&quot;It&apos;s a string....&quot;)).append(str0).append(&quot;000&quot;).toString();
str2 = (new StringBuilder(String.valueOf(str0))).append(str1).append(&quot;1111&quot;).toString();
str2 = (new StringBuilder(String.valueOf(str2))).append(&quot;1111&quot;).toString();
str2 = (new StringBuilder(String.valueOf(str2))).append(&quot;1111&quot;).toString();
for(int i = 0; i &amp;lt; 10; i++) {
    str2 = (new StringBuilder(String.valueOf(str2))).append(&quot;1111&quot;).toString();
    str2 = (new StringBuilder(String.valueOf(str2))).append(&quot;1111&quot;).toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;역컴파일 도구의 옵션에 따라서는 원래의 소스와 비슷하게 복원해서 &lt;code&gt;+&lt;/code&gt; 연산이 그대로 보일 수도 있습니다. &lt;a href=&quot;https://www.benf.org/other/cfr/stringbuilder-vs-concatenation.html&quot;&gt;cfr&lt;/a&gt;이라는 디컴파일러가 그런 경우입니다. &lt;code&gt;javap -c&lt;/code&gt; 명령으로 바이트코드를 확인하는 것이 정확합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;반복문_안에서_누적하는_문자열_결합&quot;&gt;반복문 안에서 누적하는 문자열 결합&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 역컴파일 결과에서 주목할 부분은 반복문입니다. 바이트코드에는 반복 주기마다 새로운 &lt;code&gt;StringBuilder&lt;/code&gt;를 만들고, &lt;code&gt;toString()&lt;/code&gt;을 호출해서 다음 반복에 넘길 &lt;code&gt;String&lt;/code&gt;을 만드는 코드가 들어 있습니다. HotSpot의 C2 JIT 컴파일러는 기본으로 켜져 있는 &lt;code&gt;OptimizeStringConcat&lt;/code&gt; 최적화로 &lt;code&gt;append()`와 `toString()&lt;/code&gt; 호출이 이어진 패턴을 인식해 이 임시 &lt;code&gt;StringBuilder&lt;/code&gt;의 힙 할당을 없앨 수 있으므로, 바이트코드의 &lt;code&gt;new&lt;/code&gt; 횟수가 항상 실제 객체 할당 횟수와 같지는 않습니다. 그러나 매번 더 길어진 이전 결과를 새 결과에 복사해야 한다는 문제는 남습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 성능이 중요한 반복문에서 문자열을 많이 누적할 때는 &lt;code&gt;+&lt;/code&gt; 대신 하나의 &lt;code&gt;StringBuilder&lt;/code&gt;를 직접 쓰는 편이 좋습니다. 고정된 길이의 문자열을 반복해서 붙인다면 &lt;code&gt;+&lt;/code&gt; 방식의 누적 복사량은 반복 횟수에 대해 제곱으로 증가하지만, &lt;code&gt;StringBuilder&lt;/code&gt;는 내부 버퍼를 재사용합니다. 반복 횟수가 100회 정도로 작다면 두 방식의 차이가 작으므로(아래 측정 결과 참고) 가독성이 좋은 쪽을 골라도 됩니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_3&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_3&quot; title=&quot;View footnote.&quot;&gt;3&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;StringBuilder sb = new StringBuilder(str2);
for (int i = 0; i &amp;lt; 10; i++) {
    sb.append(&quot;1111&quot;);
}
str2 = sb.toString();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 방식의 차이를 JDK 25(Temurin 25+36)에서 간단히 측정해 보았습니다. 4글자 문자열을 n회 누적하는 코드를 JIT 워밍업으로 200번 실행한 뒤, 조건마다 20번(100,000회는 5번) 실행해서 가장 짧은 시간을 기록한 결과입니다. 측정에 쓴 코드는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/string-concat&quot;&gt;examples/string-concat&lt;/a&gt;에 있습니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_4&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_4&quot; title=&quot;View footnote.&quot;&gt;4&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;반복 횟수&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;code&gt;+&lt;/code&gt; 결합&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;code&gt;StringBuilder&lt;/code&gt; 직접 사용&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;배율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;100&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.008ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.001ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;약 10배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.53ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.009ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;약 56배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;10,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;10.6ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.023ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;약 471배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;100,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1,485ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.22ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;약 6,667배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반복 횟수가 10배씩 늘 때 &lt;code&gt;+&lt;/code&gt; 결합 시간은 약 66배, 20배, 140배로 늘었고, &lt;code&gt;StringBuilder&lt;/code&gt;는 10배 이하로 늘었습니다. 누적 복사량이 반복 횟수의 제곱에 비례한다면 10배마다 100배씩 늘어야 합니다. 측정값은 JIT와 GC의 영향으로 들쭉날쭉하지만 앞의 설명과 같은 경향을 보입니다. 반복 횟수가 100회라면 두 방식 모두 0.01ms 미만이라 차이를 체감하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jdk_9부터_적용된_invokedynamic_방식&quot;&gt;JDK 9부터 적용된 invokedynamic 방식&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 9부터는 &lt;a href=&quot;https://openjdk.org/jeps/280&quot;&gt;JEP 280: Indify String Concatenation&lt;/a&gt;으로 &lt;code&gt;javac&lt;/code&gt;의 기본 변환 방식이 완전히 달라졌습니다. 컴파일러가 &lt;code&gt;StringBuilder&lt;/code&gt; 호출 코드를 만들어 넣는 대신 &lt;code&gt;invokedynamic&lt;/code&gt; 호출 지점을 남기고, 실제 결합 메서드는 실행 시점에 연결합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;invokedynamic의_동작_구조&quot;&gt;invokedynamic의 동작 구조&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;invokedynamic&lt;/code&gt;은 호출할 메서드를 실행 시점에 결정하는 바이트코드 명령입니다. &lt;code&gt;invokevirtual&lt;/code&gt;이나 &lt;code&gt;invokestatic&lt;/code&gt; 같은 다른 호출 명령은 호출 대상이 클래스 파일에 고정되어 있지만, &lt;code&gt;invokedynamic&lt;/code&gt;은 호출 지점(call site)을 어떤 메서드에 연결할지를 실행 시점의 Java 코드에 위임합니다. JVM에서 동적 언어를 지원하기 위해 &lt;a href=&quot;https://jcp.org/en/jsr/detail?id=292&quot;&gt;JSR 292&lt;/a&gt;로 JDK 7에 추가되었고, JDK 8의 람다 표현식 구현에도 쓰입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 연결을 담당하는 코드가 bootstrap 메서드입니다. 클래스 파일에는 &lt;code&gt;invokedynamic&lt;/code&gt; 호출 지점마다 사용할 bootstrap 메서드가 기록됩니다. JVM은 호출 지점을 처음 실행하기 전에 bootstrap 메서드를 호출해 링크하고, bootstrap 메서드는 실제 실행할 메서드를 가리키는 &lt;code&gt;CallSite&lt;/code&gt;를 만들어 반환합니다. 여러 스레드가 같은 호출 지점을 동시에 최초 실행하면 bootstrap 메서드가 병행 호출될 수 있지만, JVM은 그중 하나의 결과만 호출 지점에 설치합니다. 문자열 결합에서는 &lt;code&gt;StringConcatFactory.makeConcatWithConstants()&lt;/code&gt;가 bootstrap 메서드이고, 이 메서드가 결합 코드를 만들어 &lt;code&gt;CallSite&lt;/code&gt;에 담아 돌려줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;MethodHandle&lt;/code&gt;은 &lt;code&gt;java.lang.invoke&lt;/code&gt; 패키지에 있는, 실행할 대상을 가리키는 참조입니다. 메서드뿐 아니라 생성자 호출이나 필드 접근도 가리킬 수 있고, 인수와 반환 타입 정보는 &lt;code&gt;MethodType&lt;/code&gt; 객체에 담겨 있습니다. 리플렉션의 &lt;code&gt;Method&lt;/code&gt;와 쓰임새는 비슷하지만, 접근 권한을 &lt;code&gt;MethodHandle&lt;/code&gt;을 만드는 시점에 한 번만 확인하고 &lt;code&gt;invokeExact()&lt;/code&gt; 같은 메서드로 바로 호출한다는 점이 다릅니다. 또한 &lt;code&gt;MethodHandles.filterArguments()`나 `MethodHandles.foldArguments()&lt;/code&gt; 같은 메서드로 여러 &lt;code&gt;MethodHandle&lt;/code&gt;을 엮어 새로운 &lt;code&gt;MethodHandle&lt;/code&gt;을 만들 수 있습니다. 뒤에서 볼 문자열 결합 전략 중 이름이 &lt;code&gt;MH&lt;/code&gt;로 시작하는 3가지가 이 조합 기능으로 결합 코드를 구성합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;CallSite&lt;/code&gt;는 같은 패키지의 객체로, 실행할 메서드를 가리키는 &lt;code&gt;MethodHandle&lt;/code&gt; 하나를 target이라는 속성으로 참조합니다. &lt;code&gt;CallSite.getTarget()&lt;/code&gt; 메서드가 현재 target인 &lt;code&gt;MethodHandle&lt;/code&gt;을 돌려줍니다. JVM은 bootstrap 메서드가 반환한 &lt;code&gt;CallSite&lt;/code&gt;를 호출 지점에 연결해 두는데, 이를 링크라고 합니다. 링크된 뒤의 &lt;code&gt;invokedynamic&lt;/code&gt; 실행은 그 시점의 target &lt;code&gt;MethodHandle&lt;/code&gt;을 호출하는 것과 같습니다. &lt;code&gt;MutableCallSite&lt;/code&gt;처럼 실행 중에 target을 다른 &lt;code&gt;MethodHandle&lt;/code&gt;로 바꿀 수 있는 구현도 있지만, 문자열 결합에는 target이 바뀌지 않는 &lt;code&gt;ConstantCallSite&lt;/code&gt;가 쓰입니다. 그래서 한 번 링크된 뒤에는 일반 메서드 호출처럼 동작하고 JIT 컴파일러의 인라인 최적화 대상도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jdk_25에서_확인한_invokedynamic_바이트코드&quot;&gt;JDK 25에서 확인한 invokedynamic 바이트코드&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 25에서 아래의 소스를 컴파일해서 확인해 보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;StringTest.java의 인스턴스 메서드 본문&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;String str0 = &quot;It&apos;s a string....&quot;;
String str1 = &quot;It&apos;s&quot; + &quot; a string&quot; + &quot;....&quot;;
String str2 = str0 + str1 + &quot;1111&quot;;
for (int i = 0; i &amp;lt; 10; i++) {
    str2 = str2 + &quot;1111&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;javap -c StringTest&lt;/code&gt; 명령으로 바이트코드를 보면 &lt;code&gt;StringBuilder&lt;/code&gt; 호출 대신 &lt;code&gt;invokedynamic&lt;/code&gt; 명령이 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;javap -c StringTest 출력 중 해당 메서드 (JDK 25로 컴파일)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt; 0: ldc           #7    // String It&apos;s a string....
 2: astore_1
 3: ldc           #7    // String It&apos;s a string....
 5: astore_2
 6: aload_1
 7: aload_2
 8: invokedynamic #9,  0  // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
14: iconst_0
15: istore        4
17: iload         4
19: bipush        10
21: if_icmpge     37
24: aload_3
25: invokedynamic #13,  0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
30: astore_3
31: iinc          4, 1
34: goto          17
37: return&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 소스를 &lt;code&gt;javac --release 8&lt;/code&gt; 옵션으로 컴파일하면 앞 절에서 역컴파일로 보았던 것과 같은 &lt;code&gt;StringBuilder&lt;/code&gt; 호출 코드가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 방식의 장점은 결합 전략이 컴파일된 클래스 파일이 아니라 JDK 런타임에 있다는 점입니다. 클래스 파일에는 결합할 인수 자리와 상수를 적은 recipe 문자열만 남습니다. 예를 들어 위의 &lt;code&gt;str0 + str1 + &quot;1111&quot;&lt;/code&gt;은 인수 자리를 뜻하는 &lt;code&gt;\u0001&lt;/code&gt; 두 개 뒤에 &lt;code&gt;1111&lt;/code&gt;이 붙은 recipe가 되고, &lt;code&gt;javap -v&lt;/code&gt; 출력의 &lt;code&gt;BootstrapMethods&lt;/code&gt; 항목에서 확인할 수 있습니다. 그래서 한 번 컴파일한 클래스도 실행하는 JDK를 올리면 새 런타임의 결합 전략을 재컴파일 없이 적용받습니다. JDK 11로 컴파일한 클래스를 JDK 25에서 실행하면, JDK 24에서 도입된 hidden class 기반의 일반 결합 경로가 적용됩니다. 자세한 배경은 &lt;a href=&quot;https://dzone.com/articles/jdk-9jep-280-string-concatenations-will-never-be-t&quot;&gt;JDK 9/JEP 280: String Concatenations Will Never Be the Same&lt;/a&gt;에 설명되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;stringconcatfactory_결합_전략의_변화&quot;&gt;StringConcatFactory 결합 전략의 변화&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 9의 &lt;code&gt;StringConcatFactory&lt;/code&gt;에는 6가지 결합 전략이 있었고, &lt;code&gt;java.lang.invoke.stringConcat&lt;/code&gt; 시스템 프로퍼티로 선택할 수 있었습니다. 전략 이름에서 &lt;code&gt;BC&lt;/code&gt;는 실행 시점의 바이트코드 생성을, &lt;code&gt;MH&lt;/code&gt;는 &lt;code&gt;MethodHandle&lt;/code&gt; 조합을, &lt;code&gt;SB&lt;/code&gt;는 &lt;code&gt;StringBuilder&lt;/code&gt; 사용을, &lt;code&gt;SIZED&lt;/code&gt;는 버퍼 초기 용량 추정을, &lt;code&gt;SIZED_EXACT&lt;/code&gt;는 정확한 용량 계산을 뜻합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;전략&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;BC_SB&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;StringBuilder&lt;/code&gt;를 호출하는 클래스를 실행 시점에 바이트코드로 생성합니다. JDK 8까지의 &lt;code&gt;javac&lt;/code&gt;이 컴파일 시점에 만들던 코드와 같은 형태입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;BC_SB_SIZED&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;BC_SB&lt;/code&gt;와 같지만 &lt;code&gt;StringBuilder&lt;/code&gt;의 초기 용량을 추정값으로 지정해서 내부 버퍼의 확장을 줄입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;BC_SB_SIZED_EXACT&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;인수들의 길이를 미리 계산해서 &lt;code&gt;StringBuilder&lt;/code&gt;의 초기 용량을 정확한 값으로 지정합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;MH_SB_SIZED&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;바이트코드 생성 대신 &lt;code&gt;MethodHandle&lt;/code&gt; 조합으로 &lt;code&gt;StringBuilder&lt;/code&gt; 호출을 구성합니다. 초기 용량은 추정값입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;MH_SB_SIZED_EXACT&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;MH_SB_SIZED&lt;/code&gt;와 같은 방식이지만 초기 용량을 정확하게 계산합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;MH_INLINE_SIZED_EXACT&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본 전략입니다. &lt;code&gt;StringBuilder&lt;/code&gt;를 거치지 않고 최종 길이의 &lt;code&gt;byte&lt;/code&gt; 배열을 직접 만들어 인수들을 그 배열에 바로 씁니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기본 전략인 &lt;code&gt;MH_INLINE_SIZED_EXACT&lt;/code&gt;는 다 채운 &lt;code&gt;byte&lt;/code&gt; 배열로 추가 복사 없이 &lt;code&gt;String&lt;/code&gt;을 생성하므로, 임시 객체와 복사가 가장 적습니다. &lt;a href=&quot;https://github.com/openjdk/jdk/blob/34c79640e76c001e9f316f9aaba49dbe227bbae7/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java#L448&quot;&gt;StringConcatFactory.java&lt;/a&gt;의 &lt;code&gt;generateMHInlineCopy()&lt;/code&gt; 메서드가 그 구현입니다. 두 참조 값을 더하는 단순한 형태에는 &lt;a href=&quot;https://github.com/openjdk/jdk/blob/34c79640e76c001e9f316f9aaba49dbe227bbae7/src/java.base/share/classes/java/lang/StringConcatHelper.java#L406-L427&quot;&gt;StringConcatHelper.java&lt;/a&gt;의 &lt;code&gt;simpleConcat()&lt;/code&gt; 같은 특수 경로도 사용됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기본 전략 외의 나머지 5가지 선택 전략과 &lt;code&gt;java.lang.invoke.stringConcat&lt;/code&gt; 프로퍼티는 &lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8245455&quot;&gt;JDK-8245455&lt;/a&gt;로 JDK 15에서 제거되었습니다. 이 글에서는 &lt;code&gt;simpleConcat()&lt;/code&gt; 같은 특수한 형태 전용의 빠른 경로를 제외하고, 대부분의 결합이 기본으로 타는 경로를 일반 결합 경로라고 부르겠습니다. JDK 15부터 JDK 22까지의 일반 결합 경로는 &lt;code&gt;MH_INLINE_SIZED_EXACT&lt;/code&gt;에서 이어진 inline-copy MethodHandle 방식입니다. 여러 &lt;code&gt;MethodHandle&lt;/code&gt;을 조합해서, 정확한 최종 길이의 &lt;code&gt;byte&lt;/code&gt; 배열을 만들고 각 인수를 중간 버퍼 없이 그 배열에 바로 복사해 넣는(inline copy) 결합 메서드를 구성합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 23에서는 &lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8327247&quot;&gt;JDK-8327247&lt;/a&gt;로 인수 개수가 기본 임계값 20을 넘는 결합에 &lt;code&gt;StringBuilder&lt;/code&gt; 호출 바이트코드를 생성하는 경로가 추가되었습니다. 인수가 많은 결합에서는 MethodHandle 표현식 트리가 커져서, 극단적인 경우 C2 컴파일러가 이를 컴파일하는 데 최대 2GB의 메모리를 쓰는 문제가 있었습니다. 이 문제를 피하기 위한 변경입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 24에서는 &lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8336856&quot;&gt;JDK-8336856&lt;/a&gt;으로 주된 일반 결합 경로가 인수 형태(shape)별 hidden class를 생성하는 방식으로 다시 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;hidden class는 &lt;a href=&quot;https://openjdk.org/jeps/371&quot;&gt;JEP 371: Hidden Classes&lt;/a&gt;로 JDK 15에 추가된 특수한 클래스입니다. &lt;code&gt;Lookup.defineHiddenClass()&lt;/code&gt; 메서드로 정의하며, 클래스 이름으로 찾을 수 없고 참조가 사라지면 개별적으로 언로드될 수 있어서, 프레임워크나 JDK가 실행 시점에 만들어 내는 코드에 적합합니다. JDK 15부터는 람다 표현식의 구현 클래스도 hidden class로 만듭니다. JDK 25 구현에서 hidden class 캐시의 키가 되는 인수 형태는 동적 인수의 개수와 정규화된 타입으로 이루어진 &lt;code&gt;MethodType&lt;/code&gt;입니다. 참조 타입은 &lt;code&gt;Object&lt;/code&gt;로, &lt;code&gt;byte&lt;/code&gt;와 &lt;code&gt;short&lt;/code&gt;는 &lt;code&gt;int&lt;/code&gt;로 정규화됩니다. 상수의 내용과 배치는 캐시 키에 포함되지 않고 호출 지점마다 별도의 &lt;code&gt;String[]&lt;/code&gt;에 저장됩니다. 따라서 앞의 바이트코드에서 &lt;code&gt;str0 + str1 + &quot;1111&quot;&lt;/code&gt;의 호출 지점은 시그니처가 &lt;code&gt;(String, String) → String&lt;/code&gt;이지만, 캐시에서는 &lt;code&gt;(Object, Object) → String&lt;/code&gt; 형태를 사용합니다. 반복문 안 &lt;code&gt;str2 + &quot;1111&quot;&lt;/code&gt;의 호출 지점은 &lt;code&gt;(Object) → String&lt;/code&gt; 형태를 사용하므로 두 호출 지점은 서로 다른 형태입니다. 인수 형태별 hidden class 방식은 생성한 결합 클래스를 캐시에 두고 같은 형태의 다른 호출 지점에서 재사용할 수 있습니다. 캐시는 soft reference를 사용하므로 캐시된 클래스가 회수되면 같은 형태의 클래스를 다시 만들 수도 있습니다. 깊게 중첩된 &lt;code&gt;MethodHandle&lt;/code&gt; 트리 대신 JIT 컴파일러가 다루기 쉬운 평범한 바이트코드가 만들어지므로, 링크와 초기 구동(warmup) 비용이 줄어듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 25의 &lt;code&gt;StringConcatFactory&lt;/code&gt;는 먼저 단순 결합용 빠른 경로를 확인하고, 그 밖의 결합은 기본적으로 &lt;code&gt;InlineHiddenClassStrategy&lt;/code&gt;로 처리합니다. 이 전략은 동적 인수가 하나인 경우 미리 구현된 전용 핸들러를 사용하고, 그 밖의 일반적인 결합에는 인수 형태별 hidden class를 사용합니다. 결합 코드는 최종 크기의 &lt;code&gt;byte&lt;/code&gt; 배열을 채우고 그 배열로 &lt;code&gt;String&lt;/code&gt;을 만듭니다. &lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java&quot;&gt;JDK 25의 StringConcatFactory.java&lt;/a&gt;에서 이를 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한편 위의 바이트코드에서 보듯이 반복문 안의 &lt;code&gt;invokedynamic&lt;/code&gt; 명령은 반복 주기마다 이미 링크된 결합 메서드를 호출하여 새로운 결과 &lt;code&gt;String&lt;/code&gt;을 만듭니다. &lt;code&gt;StringConcatFactory&lt;/code&gt;의 bootstrap 메서드가 매번 다시 실행되는 것은 아닙니다. 컴파일러나 런타임이 누적용 &lt;code&gt;StringBuilder&lt;/code&gt; 하나를 반복문 밖에 만들어 주지는 않으므로, 많은 문자열을 누적하는 반복문에서 직접 &lt;code&gt;StringBuilder&lt;/code&gt;를 쓰라는 결론은 JDK 9 이후에도 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;JDK 버전&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;code&gt;+&lt;/code&gt; 결합 연산의 처리 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 1.4까지&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;javac&lt;/code&gt;이 컴파일 시점에 &lt;code&gt;StringBuffer&lt;/code&gt;의 &lt;code&gt;append()&lt;/code&gt;, &lt;code&gt;toString()&lt;/code&gt; 호출로 변환&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 5 ~ 8&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;javac&lt;/code&gt;이 컴파일 시점에 동기화를 하지 않는 &lt;code&gt;StringBuilder&lt;/code&gt; 호출로 변환&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 9 ~ 14&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;javac&lt;/code&gt;이 &lt;code&gt;invokedynamic&lt;/code&gt; 호출 지점을 생성. 첫 링크 시 &lt;code&gt;StringConcatFactory&lt;/code&gt;가 결합 메서드를 연결하며 6가지 전략 중 선택 가능&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 15 ~ 22&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;대안 전략을 제거하고 inline-copy MethodHandle 방식을 일반 결합 경로로 사용&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 23&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;인수가 많은 결합에는 &lt;code&gt;StringBuilder&lt;/code&gt; 호출 바이트코드를 생성하는 경로 사용&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 24 ~ 25&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;단순 결합용 빠른 경로, 단일 인수 전용 구현과 정규화된 인수 형태별 hidden class 기반의 일반 결합 경로 사용&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문자열 타입의 컴파일 타임 상수식이 하나의 상수로 합쳐지는 동작은 모든 버전에서 같습니다. 반복문 밖에서 문자열 한두 개를 더하는 코드는 &lt;code&gt;javac&lt;/code&gt;과 JDK 런타임이 최적화할 수 있으므로 가독성이 좋은 &lt;code&gt;+&lt;/code&gt; 연산을 그대로 쓰면 됩니다. 반면 컴파일러나 런타임이 반복문을 누적용 &lt;code&gt;StringBuilder&lt;/code&gt; 하나로 자동 변환하지는 않습니다. 반복 횟수가 1,000회 이상인 반복문에서 문자열을 누적한다면 &lt;code&gt;StringBuilder&lt;/code&gt;를 직접 쓰는 편이 좋습니다. 앞의 측정에서는 1,000회에서 약 56배, 10,000회에서 약 471배 차이가 났습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javame/config/cldc/ref-impl/cldc1.1/jsr139/java/lang/StringBuffer.html&quot;&gt;CLDC 1.1 StringBuffer javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.18.1&quot;&gt;JLS 15.18.1: String Concatenation Operator +&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.29&quot;&gt;JLS 15.29: Constant Expressions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jcp.org/en/jsr/detail?id=292&quot;&gt;JSR 292: Supporting Dynamically Typed Languages on the Java Platform&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/280&quot;&gt;JEP 280: Indify String Concatenation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/371&quot;&gt;JEP 371: Hidden Classes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dzone.com/articles/jdk-9jep-280-string-concatenations-will-never-be-t&quot;&gt;JDK 9/JEP 280: String Concatenations Will Never Be the Same&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8245455&quot;&gt;JDK-8245455: Remove alternative StringConcatFactory strategies&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8327247&quot;&gt;JDK-8327247: C2 uses up to 2GB of RAM to compile complex string concat in extreme cases&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8336856&quot;&gt;JDK-8336856: Efficient hidden class-based string concatenation strategy&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java&quot;&gt;JDK 25 StringConcatFactory.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://pellegrino.link/2015/08/22/string-concatenation-with-java-8.html&quot;&gt;String concatenation with Java 8&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.benf.org/other/cfr/stringbuilder-vs-concatenation.html&quot;&gt;CFR: StringBuilder vs concatenation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;footnotes&quot;&gt;
&lt;hr&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_1&quot;&gt;
&lt;a href=&quot;#_footnoteref_1&quot;&gt;1&lt;/a&gt;. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.18.1&quot;&gt;JLS 15.18.1&lt;/a&gt;은 SE 25판에서도 컴파일러가 &lt;code&gt;StringBuffer&lt;/code&gt;나 그와 비슷한 기법을 쓸 수 있다고만 설명합니다. &lt;code&gt;StringBuilder&lt;/code&gt;와 &lt;code&gt;invokedynamic&lt;/code&gt;도 여기서 말하는 비슷한 기법(similar technique)에 해당합니다.
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_2&quot;&gt;
&lt;a href=&quot;#_footnoteref_2&quot;&gt;2&lt;/a&gt;. 컴파일 타임 상수식의 정확한 범위는 &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.29&quot;&gt;JLS 15.29&lt;/a&gt;를 참고합니다.
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_3&quot;&gt;
&lt;a href=&quot;#_footnoteref_3&quot;&gt;3&lt;/a&gt;. 성능 테스트 사례는 &lt;a href=&quot;https://pellegrino.link/2015/08/22/string-concatenation-with-java-8.html&quot;&gt;String concatenation with Java 8&lt;/a&gt;을 참고합니다.
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_4&quot;&gt;
&lt;a href=&quot;#_footnoteref_4&quot;&gt;4&lt;/a&gt;. JMH를 쓰지 않은 &lt;code&gt;System.nanoTime()&lt;/code&gt; 기반의 단순 측정이라 미세한 수치는 부정확할 수 있지만, 10,000회 이상에서는 수백 배 이상 차이가 나므로 결론에는 영향이 없습니다.
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Claude Code에서 작업 중에 보낸 프롬프트가 전달되는 시점</title>
      <link>https://blog.benelog.net//claude-code-steering.html</link>
      <pubDate>Sat, 22 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">claude-code-steering.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#btw와_큐_메시지의_차이&quot;&gt;&lt;code&gt;/btw&lt;/code&gt;와 큐 메시지의 차이&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#큐에_넣은_메시지가_모델에_전달되는_시점&quot;&gt;큐에 넣은 메시지가 모델에 전달되는 시점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#changelog에서_확인한_steering_관련_변경_이력&quot;&gt;CHANGELOG에서 확인한 steering 관련 변경 이력&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code가 작업하는 중에 무언가를 덧붙이고 싶을 때 한동안 저는 &lt;code&gt;/btw&lt;/code&gt;를 썼습니다. 그냥 프롬프트를 입력하면 그 작업에 반영되지 않는다고 느꼈기 때문입니다. 그런데 어느 시점부터인가 &lt;code&gt;/btw&lt;/code&gt; 없이 그냥 입력해도 작업 중간에 잘 반영되었습니다. 현시점에서의 &lt;code&gt;/btw&lt;/code&gt;를 언제써야할지와  릴리즈 노트에서 관련 이력을 찾아봤습니다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;btw와_큐_메시지의_차이&quot;&gt;&lt;code&gt;/btw&lt;/code&gt;와 큐 메시지의 차이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/interactive-mode&quot;&gt;Interactive mode 문서&lt;/a&gt;에 따르면 두 기능은 이렇게 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;code&gt;/btw&lt;/code&gt; (side question)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;작업 중 입력한 프롬프트 (queued message)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;대화 이력 반영&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;반영되지 않음. 질문과 답변 모두 conversation history에 들어가지 않음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;반영됨. 일반 메시지와 같은 자격으로 대화에 들어감&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도구 사용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;불가. 이미 컨텍스트에 있는 내용만으로 답함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;가능. 파일 읽기, 명령 실행 모두 가능&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;진행 중인 작업에 미치는 영향&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음. 본 작업을 중단시키지도, 방향을 바꾸지도 않음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;있음. 이후 작업의 방향이 바뀜&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;용도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;이번 세션에서 이미 알고 있는 내용을 묻기&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;작업 방향을 바꾸거나 지시를 덧붙이기&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문서는 &lt;code&gt;/btw&lt;/code&gt;를 &quot;subagent의 반대&quot;라고 설명합니다. subagent는 대화 이력 없이 도구만 갖고 새로 알아내는 쪽이고, &lt;code&gt;/btw&lt;/code&gt;는 대화 이력을 다 보지만 도구가 없는 쪽입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 &lt;code&gt;/btw&lt;/code&gt;로 지시를 내리면 그 지시는 진행 중인 작업에 반영되지 않습니다. 제 프롬프트 이력에도 &lt;code&gt;/btw&lt;/code&gt;로 지시를 내린 뒤 &quot;아까 btw로 내린 명령을 수행해 줘&quot;라고 다시 요청한 기록이 남아 있었습니다. 애초에 지시를 전달하는 용도로는 맞지 않는 명령을 썼던 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;/btw&lt;/code&gt; 자체가 언제 도입되었는지는 CHANGELOG에서 확인되지 않습니다. 도입을 알리는 항목이 없고, 가장 이른 언급은 v2.1.79(2026-03-18)의 버그 수정입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Fixed `/btw` returning the main agent&apos;s output instead of answering
the side question when triggered during streaming&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;큐에_넣은_메시지가_모델에_전달되는_시점&quot;&gt;큐에 넣은 메시지가 모델에 전달되는 시점&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;작업 중에 프롬프트를 입력하고 Enter를 누르면 Claude Code는 턴을 중단하지 않고 큐에 넣습니다. 큐에 있는 항목이 모델에 언제 전달되는지는 문서에 이렇게 적혀 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Messages: if you queue a message while Claude is running tool calls, Claude Code passes it to Claude as soon as those tool calls finish, within the same turn. When the turn ends, Claude Code sends the messages that are still queued as the next turn, each as a separate message&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉 도구 호출이 진행 중일 때 넣은 메시지는 그 도구 호출이 끝나는 시점에 &lt;strong&gt;같은 턴 안에서&lt;/strong&gt; 전달됩니다. 턴이 이미 끝나 가고 있었다면 다음 턴의 첫 메시지가 됩니다. 슬래시 명령과 &lt;code&gt;!&lt;/code&gt; 셸 명령은 다르게 처리되어 턴이 끝난 뒤에 하나씩 실행됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;바로 전달하고 싶으면 Esc로 턴을 중단하면 됩니다. 이때 큐에 있던 내용은 버려지지 않고 그대로 전달됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 동작이 언제부터 이랬는지는 문서에 나와 있지 않습니다. 이 문서는 동작이 바뀐 항목에 &quot;Before v2.1.186&quot;, &quot;Before v2.1.212&quot;처럼 이전 버전 주석을 붙이는데, 큐 관련 절에는 그런 주석이 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;changelog에서_확인한_steering_관련_변경_이력&quot;&gt;CHANGELOG에서 확인한 steering 관련 변경 이력&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;작업 중 메시지 전달과 직접 관련된 항목은 세 개입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;버전&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;배포일&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;릴리즈 노트 항목&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0.2.108&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2025-05-13&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;You can now send messages to Claude while it works to steer Claude in real-time&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.0.84&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2025-08-18&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Fix Claude sometimes ignoring real-time steering when wrapping up a task&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2.1.20&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2026-01-27&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Fixed agents sometimes ignoring user messages sent while actively working on a task&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;작업 중 메시지를 실시간으로 보내는 기능 자체는 2025년 5월에 도입되었습니다. &quot;보낸 메시지를 무시한다&quot;는 문제를 정면으로 고친 항목은 v2.1.20이 마지막입니다. 한 버전을 꼽으라면 이쪽입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 뒤로는 기능 변경이 아니라 유실이나 표시 관련 수정이 이어졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;v2.1.97(2026-04-08): 작업 중 입력한 메시지가 transcript에 기록되지 않던 문제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.105(2026-04-13): 큐에 넣은 메시지에 첨부한 이미지가 버려지던 문제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.178(2026-06-15): subagent가 턴을 마무리하는 중에 보낸 메시지가 유실되던 문제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.205(2026-07-08): &lt;code&gt;--max-turns&lt;/code&gt; 한도에서 턴이 끝날 때 작업 중 보낸 메시지가 조용히 사라지던 문제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.234(2026-08-17): 큐에 남아 있는 메시지가 프롬프트 이력에 다시 나타나던 문제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.239(2026-08-21): 큐에 프롬프트가 있는 상태에서 Esc를 눌렀을 때 턴이 일찍 끝나던 경합 문제&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/interactive-mode&quot;&gt;Claude Code Interactive mode 문서&lt;/a&gt; : &quot;Queue messages while Claude works&quot;, &quot;Side questions with /btw&quot; 절&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code CHANGELOG&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://registry.npmjs.org/@anthropic-ai/claude-code&quot;&gt;npm 레지스트리의 @anthropic-ai/claude-code 메타데이터&lt;/a&gt; : 버전별 배포 시각&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Cloud Run과 Turso로 개인 앱 배포하기</title>
      <link>https://blog.benelog.net//cloud-run-turso-app.html</link>
      <pubDate>Sat, 22 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">cloud-run-turso-app.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#cloud_run과_turso를_고른_이유&quot;&gt;1. Cloud Run과 Turso를 고른 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#앱_구성과_배포_설정_절차&quot;&gt;2. 앱 구성과 배포 설정 절차&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#cloud_run_배포의_빌드_권한과_healthz_문제&quot;&gt;3. Cloud Run 배포의 빌드 권한과 /healthz 문제&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#cloud_run_요금이_0원을_넘어가는_조건&quot;&gt;4. Cloud Run 요금이 0원을 넘어가는 조건&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#응답_시간을_좌우한_리전과_프록시&quot;&gt;5. 응답 시간을 좌우한 리전과 프록시&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#선택하지_않은_배포처&quot;&gt;6. 선택하지 않은 배포처&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#이_구성의_제약&quot;&gt;7. 이 구성의 제약&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;8. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;혼자 작성할 TIL(Today I Learned) 기록장을 만들어 &lt;a href=&quot;https://til.benelog.net&quot;&gt;til.benelog.net&lt;/a&gt;에 올렸습니다.
이 과정에서 애플리케이션 서버와 DB를 어떤 인프라 위에서 실행할지를 두고 여러 기술을 검토했습니다.
무료로 운영할 수 있어야 한다는 점을 가장 중요시했습니다.
무료 조건 내에서는 접속 속도도 상대적으로 빠르기를 바랬습니다.
또한 로컬 개발 환경과 운영 환경에서 최대한 같은 코드 경로를 사용해, 배포하기 전에 로컬에서 많은 동작을 검증할 수 있어야 한다는 조건도 두었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에는 카드 등록까지 피하려 했지만 이 조건을 모두 만족하는 배포처는 찾지 못했습니다.
그래서 카드가 등록된 결제 계정은 연결하되 각 서비스의 무료 한도 안에서 쓰는 것으로 기준을 낮췄고, Google Cloud Run과 Turso를 조합했습니다.
도메인은 처음에 Netlify 프록시로 붙였다가, 응답 시간을 측정한 뒤 Cloud Run의 직접 커스텀 도메인 매핑으로 옮겼습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;애플리케이션 서버의 소스는 &lt;a href=&quot;https://github.com/benelog/til&quot;&gt;benelog/til&lt;/a&gt;에 있습니다.
요금과 무료 한도, 지원 리전은 2026년 8월 22일에 확인한 값입니다. 바뀔 수 있으므로 이 문서를 참고하실 분은 실제로 기술을 선택하시기 전에 글 끝의 공식 문서를 다시 확인하시는 것을 추천드립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cloud_run과_turso를_고른_이유&quot;&gt;1. Cloud Run과 Turso를 고른 이유&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;애플리케이션 서버는 Cloud Run에 배포합니다.
요청 기반 과금의 무료 한도는 Tier 1 단가 기준으로 월 2M 요청, 360,000 GiB-초, 180,000 vCPU-초입니다.
최소 인스턴스를 지정하지 않아 요청이 없을 때는 0개로 축소되기에 이 앱의 실제 트래픽은 무료 한도 안에 충분히 들어갈 것으로 예상했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run 서비스 사용량 외에도 함께 청구되는 항목이 있고, 설정에 따라 과금 방식 자체가 달라집니다.
트래픽이 작다는 이유만으로 비용이 언제나 0원이라고 보장되지는 않습니다.
어떤 조건에서 금액이 붙는지는 뒤의 &quot;Cloud Run 요금이 0원을 넘어가는 조건&quot; 섹션에 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서울 리전(&lt;code&gt;asia-northeast3&lt;/code&gt;)을 쓸 수 있다는 점도 최초의 선택 이유였습니다.
최종 운영 리전은 도쿄(&lt;code&gt;asia-northeast1&lt;/code&gt;)입니다. 서울과 도쿄의 응답 시간 차이보다 도메인 연결 방식의 차이가 컸기 때문인데, 근거는 뒤의 &quot;응답 시간을 좌우한 리전과 프록시&quot; 섹션에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;데이터 저장소는 Turso(libSQL)입니다.
Cloud Run 컨테이너의 쓰기 가능한 파일 시스템은 영속적으로 유지되지 않습니다.
로컬 SQLite 파일을 그 안에서 쓰면 서버 인스턴스가 종료될 때 데이터가 없어지고, 여러 인스턴스가 뜨면 서로 다른 파일을 보게 됩니다.
따라서 영속 데이터를 컨테이너 밖에 두어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Turso는 SQLite와 호환되는 원격 저장소이고, 무료 한도가 SQLite 파일 하나 분량의 데이터를 다루는 개인 앱에는 충분합니다.
무료 요금제에서는 데이터베이스 100개까지, 전체 저장 용량 5GB까지 쓸 수 있습니다.
드라이버(&lt;code&gt;libsql-client-go&lt;/code&gt;)가 순수 Go라서 Go로 만든 앱을 CGO 없이 빌드하는 구성이 그대로 유지됩니다.
앱은 환경변수에 Turso URL이 있으면 libSQL 드라이버를, 없으면 로컬 SQLite 드라이버를 고릅니다.
드라이버와 접속 문자열만 달라지고, `database/sql`로 질의하는 저장소 코드는 두 환경에서 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;도메인은 Cloud Run의 커스텀 도메인 매핑으로 직접 붙였습니다.
처음에는 서울 서비스 앞에 기존 Netlify 사이트를 프록시로 두었습니다. Cloud Run의 직접 매핑이 서울 리전을 지원하지 않기 때문입니다.
측정해 보니 이 프록시가 응답 시간의 상당 부분을 차지해서, 직접 매핑을 지원하는 도쿄로 서비스를 옮기고 프록시를 걷어냈습니다.
그 과정과 측정값은 뒤의 &quot;응답 시간을 좌우한 리전과 프록시&quot; 섹션에서 다룹니다.
&lt;code&gt;benelog.net&lt;/code&gt;의 DNS zone은 여전히 Netlify가 관리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;앱_구성과_배포_설정_절차&quot;&gt;2. 앱 구성과 배포 설정 절차&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앱은 Go 표준 &lt;code&gt;net/http&lt;/code&gt;와 htmx로 만들었습니다.
웹 프레임워크는 쓰지 않았고, 화면 갱신은 htmx가 HTML 조각을 갈아 끼우는 방식입니다.
의존성이 전부 순수 Go라 CGO 없이 정적 바이너리로 빌드됩니다.
런타임 이미지로 &lt;code&gt;gcr.io/distroless/static-debian12&lt;/code&gt;를 쓰고 그 위에 바이너리 하나만 올리므로, 언어 런타임 레이어가 아예 없습니다.
Docker 이미지의 크기는 28MB입니다.
같은 앱을 Python이나 Node.js로 만들었다면 런타임을 담은 베이스 이미지 때문에 전체 크기가 훨씬 커집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;런타임 구성&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;베이스 이미지 압축 크기&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;앱까지 포함한 크기(압축 해제)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;distroless static + Go 정적 바이너리&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;약 8MB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;28MB&lt;/strong&gt; (실측)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;python:3.13-alpine&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;16.1MB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;90~130MB (추정)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;python:3.13-slim&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;41.0MB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;170~210MB (추정)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;node:22-alpine&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;55.1MB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;190~230MB (추정)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;node:22-slim&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;76.2MB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;250~290MB (추정)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;베이스 이미지 크기는 Docker Hub에서 조회한 amd64 값이고, 앱까지 포함한 크기는 웹 프레임워크·마크다운·HTML sanitizer·libSQL 클라이언트를 더했다고 가정해 역산한 추정치입니다.
실제로 빌드해 본 값은 Go 구성뿐입니다.
이미지 크기가 Cloud Run 요금에 직접 반영되지는 않지만, 뒤에서 설명할 Artifact Registry 무료 한도와 콜드 스타트에는 영향을 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 앱에서 목록과 각 기록을 읽는 요청은 모든 접속자에게 공개하고, 작성·수정·삭제 요청은 단일 비밀번호로 로그인한 세션만 허용합니다.
따라서 뒤에서 쓰는 &lt;code&gt;--allow-unauthenticated&lt;/code&gt;는 Cloud Run의 IAM 인증을 요구하지 않는다는 뜻이지, 쓰기 기능까지 인증 없이 공개한다는 뜻은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/til/til-list.png&quot; alt=&quot;TIL 기록 목록 화면&quot; width=&quot;600&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/til/til-edit.png&quot; alt=&quot;기록 편집 화면. 마크다운 입력과 미리보기&quot; width=&quot;600&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포를 위한 설정 과정은 다음 순서로 따라할 수 있습니다.
Turso와 Google Cloud 가입, &lt;a href=&quot;https://docs.turso.tech/cli/installation&quot;&gt;turso CLI&lt;/a&gt;와 &lt;a href=&quot;https://cloud.google.com/sdk/docs/install&quot;&gt;gcloud CLI&lt;/a&gt; 설치, 각 CLI의 로그인은 마쳤다고 전제합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;Turso에서 도쿄 리전 그룹과 데이터베이스를 만들고 URL과 토큰을 확인합니다. Turso가 제공하는 리전에 서울은 없어서, 가장 가까운 도쿄를 골랐습니다.&lt;/p&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;turso group create tokyo --location aws-ap-northeast-1
turso db create til --group tokyo
turso db show til --url
turso db tokens create til&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;사용할 GCP 프로젝트를 선택하고 결제 계정을 연결한 뒤 API 세 개를 켭니다. 결제 계정 연결은 무료 한도만 쓸 때도 필요합니다.&lt;/p&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gcloud config set project &amp;lt;project&amp;gt;
gcloud billing projects link &amp;lt;project&amp;gt; --billing-account=&amp;lt;ACCOUNT_ID&amp;gt;
gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;기본 컴퓨트 서비스 계정에 빌드 권한을 부여합니다. 이 단계를 건너뛰면 첫 배포가 실패합니다.&lt;/p&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gcloud projects add-iam-policy-binding &amp;lt;project&amp;gt; \
  --member=serviceAccount:&amp;lt;projectNumber&amp;gt;-compute@developer.gserviceaccount.com \
  --role=roles/cloudbuild.builds.builder&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;저장소 루트에서 환경변수 파일을 만들고 배포합니다. &lt;code&gt;--source .&lt;/code&gt;가 Cloud Build로 Dockerfile을 빌드합니다.&lt;/p&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;/tmp/run-env.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;TURSO_DATABASE_URL: &quot;libsql://&amp;lt;database-host&amp;gt;&quot;
TURSO_AUTH_TOKEN: &quot;&amp;lt;turso-db-token&amp;gt;&quot;
TIL_PASSWORD: &quot;&amp;lt;write-password&amp;gt;&quot;
TZ: &quot;Asia/Seoul&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 파일에는 DB 쓰기 권한이 있는 토큰과 로그인 비밀번호가 평문으로 들어갑니다.
저장소 안에 만들거나 커밋하지 않고, 파일 권한을 제한한 임시 경로에 둡니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gcloud run deploy til-tokyo --source . --region asia-northeast1 \
  --allow-unauthenticated --env-vars-file /tmp/run-env.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Google Search Console에서 &lt;code&gt;benelog.net&lt;/code&gt;의 소유권을 도메인 방식으로 확인합니다. Cloud Run은 검증된 도메인에만 매핑을 만들 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;서비스에 커스텀 도메인을 매핑합니다. 이 기능은 도쿄처럼 지원하는 리전에서만 쓸 수 있습니다. 매핑 명령은 &lt;code&gt;beta&lt;/code&gt; 컴포넌트에 들어 있어서 없으면 먼저 설치해야 합니다. 이름이 비슷한 &lt;code&gt;gcloud run domain-mappings&lt;/code&gt;는 Cloud Run for Anthos용이라 관리형 Cloud Run에는 쓸 수 없습니다.&lt;/p&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gcloud components install beta

gcloud beta run domain-mappings create \
  --domain=til.benelog.net --service=til-tokyo \
  --region=asia-northeast1 --project=&amp;lt;project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;매핑이 안내한 레코드를 DNS에 넣습니다. 저는 &lt;code&gt;benelog.net&lt;/code&gt; zone을 Netlify에서 관리하고 있어서 Netlify의 DNS 화면에서 추가했습니다.&lt;/p&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;til  CNAME  ghs.googlehosted.com.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CNAME은 같은 이름의 다른 레코드와 공존할 수 없습니다. 그 이름에 이미 다른 레코드가 있으면 먼저 지워야 합니다.
관리형 인증서는 DNS가 &lt;code&gt;ghs.googlehosted.com.&lt;/code&gt;을 가리킨 뒤에야 발급되기 시작합니다. 그 사이에 그 호스트로 접속하면 TLS 오류가 납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;환경변수를 파일로 넘기면 토큰 값 자체는 셸 명령 이력에 남지 않습니다.
Cloud Run 서비스 설정에는 환경변수가 저장되므로 접근 권한이 있는 사용자는 값을 볼 수 있습니다.
더 엄격하게 관리해야 하는 서비스라면 Secret Manager를 연결해야 합니다.
한 번 넣은 값은 다음 리비전에도 이어지므로, 값을 바꾸지 않는 다음 배포부터는 &lt;code&gt;--env-vars-file&lt;/code&gt;을 생략할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cloud_run_배포의_빌드_권한과_healthz_문제&quot;&gt;3. Cloud Run 배포의 빌드 권한과 /healthz 문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음 배포할 때는 다음의 두 가지 문제를 만났습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;기본_서비스_계정의_빌드_권한_부족&quot;&gt;3.1. 기본 서비스 계정의 빌드 권한 부족&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gcloud run deploy --source .&lt;/code&gt;의 첫 실행이 실패했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;ERROR: (gcloud.run.deploy) PERMISSION_DENIED: Build failed because the default
service account is missing required IAM permissions.
... IAM permission denied for service account
788537242173-compute@developer.gserviceaccount.com&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Google이 기본 컴퓨트 서비스 계정의 빌드 권한을 축소한 뒤 새 프로젝트에서 나타나는 증상입니다.
&lt;code&gt;roles/cloudbuild.builds.builder&lt;/code&gt;를 한 번 부여하면 해결됩니다.
앞의 배포 설정 절차 3단계가 이것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;google_frontend가_가로채는_healthz_경로&quot;&gt;3.2. Google Frontend가 가로채는 /healthz 경로&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포 후 Cloud Run이 발급한 &lt;code&gt;run.app&lt;/code&gt; 주소에서 헬스 체크 경로를 확인하니 앱이 아니라 Google의 404 페이지가 돌아왔습니다.
앱은 정상이어서 &lt;code&gt;/&lt;/code&gt;도 기록 목록도 잘 나오는데 &lt;code&gt;/healthz&lt;/code&gt;만 그랬습니다.
경로를 바꿔 가며 확인한 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;경로&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;응답&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;누가 응답했나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;/healthz&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;404&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Google의 404 HTML. 컨테이너까지 도달하지 않습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;/healthz/&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;404&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;404 page not found&lt;/code&gt;. Go의 &lt;code&gt;ServeMux&lt;/code&gt;가 응답했습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/readyz&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;404&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;404 page not found&lt;/code&gt;. 역시 앱까지 도달합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;/entries/2&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;200&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;앱이 정상 응답합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 서비스에서 확인한 경로 중에는 &lt;strong&gt;정확히 &lt;code&gt;/healthz&lt;/code&gt; 하나만&lt;/strong&gt; 앞단에서 가로챘습니다.
슬래시를 붙인 &lt;code&gt;/healthz/&lt;/code&gt;는 그대로 통과하므로, 앱의 라우팅 문제가 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;/healthz&lt;/code&gt;는 Cloud Run이 요구한 경로가 아니라, 제가 관례를 따라 앱에 정한 경로입니다.
이 관례는 Google 내부의 z-pages(&lt;code&gt;/healthz&lt;/code&gt;, &lt;code&gt;/varz&lt;/code&gt;, &lt;code&gt;/statusz&lt;/code&gt;)에서 나왔습니다.
실제 애플리케이션 경로와 충돌하지 않도록 z를 붙인 이름인데, Kubernetes의 liveness probe 예제와 여러 헬스 체크 라이브러리를 거치며 업계 표준처럼 퍼졌습니다.
Cloud Run의 공식 &quot;Known issues&quot;에도 &lt;code&gt;/eventlog&lt;/code&gt;, &lt;code&gt;/_ah/&lt;/code&gt;로 시작하는 경로와 일부 &lt;code&gt;z&lt;/code&gt;로 끝나는 경로를 쓸 수 없다고 나옵니다.
즉, &lt;code&gt;/healthz&lt;/code&gt; 하나만의 특수 규칙으로 일반화하기보다 Cloud Run에서는 &lt;code&gt;z&lt;/code&gt;로 끝나는 진단 경로를 피하는 편이 안전합니다.
기본 헬스 경로가 &lt;code&gt;/healthz&lt;/code&gt;인 Streamlit도 GCP에서 같은 문제를 겪고 경로를 바꾸는 옵션을 추가했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run 자체가 이 경로를 요구하지는 않습니다.
다만 외부 모니터나 Cloud Run의 HTTP startup/liveness probe가 &lt;code&gt;/healthz&lt;/code&gt;를 호출하도록 설정돼 있다면 컨테이너가 정상이어도 계속 404로 판단합니다.
그래서 같은 핸들러를 &lt;code&gt;/health&lt;/code&gt;에도 등록했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;main.go&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;// Cloud Run 앞단의 Google Frontend 가 정확히 /healthz 경로를 가로채 자체 404 를
// 돌려주므로(다른 경로는 그대로 통과) 헬스 경로를 /health 로도 열어둔다.
health := func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte(&quot;ok&quot;))
}
mux.HandleFunc(&quot;GET /healthz&quot;, health)
mux.HandleFunc(&quot;GET /health&quot;, health)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cloud_run_요금이_0원을_넘어가는_조건&quot;&gt;4. Cloud Run 요금이 0원을 넘어가는 조건&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run에서 금액이 붙기 시작하는 조건을 나눠서 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;무료_한도가_적용되는_방식&quot;&gt;4.1. 무료 한도가 적용되는 방식&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;무료 한도는 프로젝트가 아니라 &lt;strong&gt;결제 계정 단위로 합산&lt;/strong&gt;되고 매월 초기화됩니다.
요청 기반 과금(request-based billing)의 무료 한도는 CPU 180,000 vCPU-초, 메모리 360,000 GiB-초, 요청 2,000,000건입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서 놓치기 쉬운 조건이 두 가지 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째, 무료 한도는 사용량을 빼 주는 것이 아니라 &lt;strong&gt;Tier 1 단가에 해당하는 금액을 할인&lt;/strong&gt;하는 방식입니다.
Tier 2 리전에서 같은 양을 쓰면 단가가 더 비싸므로 할인으로 덮이는 사용량이 그만큼 줄어듭니다.
서울(&lt;code&gt;asia-northeast3&lt;/code&gt;)은 Tier 2, 도쿄(&lt;code&gt;asia-northeast1&lt;/code&gt;)는 Tier 1입니다.
응답 시간 때문에 도쿄로 옮긴 것이지만, 결과적으로 무료 한도를 온전히 쓰는 리전이 되기도 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 과금 시간은 &lt;strong&gt;100밀리초 단위로 올림&lt;/strong&gt;됩니다.
요청 기반 과금에서는 요청을 처리하는 동안(billable instance time)에만 CPU와 메모리가 계산되고, 컨테이너에 도달하지 못한 요청은 과금되지 않습니다.
IAM 정책에서 거부된 요청도 마찬가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;설정_하나로_과금_방식이_바뀌는_항목&quot;&gt;4.2. 설정 하나로 과금 방식이 바뀌는 항목&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;기본값에서 달라지는 것&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;이 앱의 선택&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;최소 인스턴스&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1개 이상으로 올리면 요청이 없는 동안에도 idle 단가로 과금됩니다. 최소 인스턴스가 아닌 유휴 인스턴스는 과금되지 않습니다.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;지정하지 않았습니다. 대신 첫 요청에 콜드 스타트가 포함됩니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;인스턴스 기반 과금&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;opt-in하면 요청 처리 여부와 무관하게 인스턴스가 살아 있는 &lt;strong&gt;전체 수명&lt;/strong&gt; 동안, 최소 1분 단위로 과금됩니다. 무료 한도도 CPU 240,000 vCPU-초 / 메모리 450,000 GiB-초로 달라지고 요청 수 무료 한도는 없습니다.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본값인 요청 기반 과금을 씁니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;동시 실행 수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;낮추면 같은 요청 수를 처리하는 데 인스턴스가 더 많이 필요하고, 그만큼 인스턴스 시간이 늘어납니다.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본값을 씁니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;CPU·메모리 크기&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;vCPU-초와 GiB-초 단가에 그대로 곱해집니다. 무료 한도에 닿는 속도도 같은 배수로 빨라집니다.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본값을 씁니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;cloud_run_청구서_밖에서_늘어나는_항목&quot;&gt;4.3. Cloud Run 청구서 밖에서 늘어나는 항목&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gcloud run deploy --source .&lt;/code&gt; 한 줄로 배포해도 Cloud Run 외의 서비스가 함께 쓰이고, 각각 별도의 요금 체계를 가집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;무료 한도&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;이 앱의 사용량&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cloud Build&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;결제 계정당 월 2,500 build-분(기본 풀의 &lt;code&gt;e2-standard-2&lt;/code&gt; 기준. promotional이라 바뀔 수 있음)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;배포할 때만 실행되고 한 번에 몇 분입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Artifact Registry&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;결제 계정당 0.5 GiB-월. 초과분은 GiB-시간당 $0.000136986&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cloud-run-source-deploy&lt;/code&gt; 저장소가 18.9MB입니다. 리비전을 배포할 때마다 이미지가 쌓입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cloud Storage&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;소스 배포는 &lt;code&gt;run-sources-&amp;lt;프로젝트&amp;gt;-&amp;lt;리전&amp;gt;&lt;/code&gt; 버킷에 소스 압축본을 보관합니다. 리전 버킷이라 아시아 리전에는 상시 무료 한도가 없습니다.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;배포마다 수십 KB 수준입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cloud Logging&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;프로젝트당 월 50 GiB. 초과분은 GiB당 $0.50&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;요청 로그만 남습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;외부 데이터 전송&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;목적지에 따라 다릅니다. 아래에 따로 정리했습니다.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;한국으로 나가는 응답에는 무료 구간이 없습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 중 Cloud Storage와 외부 데이터 전송에는 이 구성에서 기대할 수 있는 상시 무료 한도가 없습니다.
개인 앱의 사용량에서는 반올림돼 0원으로 남지만, &quot;무료 한도 안에서 쓰고 있다&quot;고 말할 수 있는 항목은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;목적지에_따라_달라지는_외부_전송_요금&quot;&gt;4.4. 목적지에 따라 달라지는 외부 전송 요금&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run에서 나가는 인터넷 전송은 Premium Tier로 Google Cloud 네트워킹 요금이 적용됩니다.
들어오는 트래픽은 무료이고, 나가는 트래픽만 GiB 단위로 계산합니다.
중요한 점은 &lt;strong&gt;단가가 출발 리전이 아니라 목적지 기준&lt;/strong&gt;이라는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;트래픽이 도착하는 곳(목적지)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;무료 구간&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;1GiB ~ 1,024GiB 단가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아시아(한국·인도네시아 제외). 일본 포함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0 ~ 1GiB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.12 / GiB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;북미&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0 ~ 1GiB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.12 / GiB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;유럽&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0 ~ 1GiB&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.12 / GiB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;한국&lt;/strong&gt;, 호주, 인도네시아, 남미, 사우디아라비아&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;없음&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.19 / GiB (0GiB부터)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앱을 어느 리전에 두든 이 구분은 달라지지 않습니다.
서울에서 한국 사용자에게 보내도 같은 $0.19/GiB이고, 서울에서 도쿄의 Turso로 보내도 같은 $0.12/GiB입니다.
Turso는 AWS에 있어서 Google 밖의 주소이므로, 도쿄에서 같은 도쿄로 나가더라도 사설 구간이 아니라 인터넷 전송으로 계산됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 앱에서 나가는 트래픽은 두 갈래입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Turso로 보내는 질의&lt;/strong&gt;: 목적지가 도쿄(일본)라서 아시아 단가가 적용됩니다. 다만 나가는 것은 SQL 질의문이라 매우 작고, 결과 행은 들어오는 방향이라 무료입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;사용자 브라우저로 보내는 응답&lt;/strong&gt;: 독자가 대부분 한국에 있으므로 무료 구간 없이 $0.19/GiB 구간에 들어갑니다. 실제로는 이쪽이 전송량의 대부분입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;netlify_프록시를_걷어내면서_나빠진_전송_조건&quot;&gt;4.5. Netlify 프록시를 걷어내면서 나빠진 전송 조건&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 표를 이번 전환에 대입해 보면, 프록시를 걷어낸 결정이 응답 시간에는 이득이지만 전송 요금에는 손해입니다.
두 가지가 함께 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;목적지가 싱가포르에서 한국으로 바뀌었습니다.&lt;/strong&gt;
전환 전 &lt;code&gt;til.benelog.net&lt;/code&gt;의 A 레코드는 &lt;code&gt;13.215.239.219&lt;/code&gt;였고, RDAP로 조회하면 등록명이 &lt;code&gt;AMAZON-SIN&lt;/code&gt;입니다.
즉 Cloud Run이 응답을 건네던 상대는 한국의 사용자가 아니라 싱가포르에 있는 Netlify의 엣지였습니다.
GCP 입장의 목적지가 싱가포르였으므로 아시아 단가 $0.12/GiB에 0~1GiB 무료 구간이 적용됐고, 싱가포르에서 한국까지의 마지막 구간은 Netlify의 대역폭이었습니다.
직접 매핑으로 바꾼 지금은 목적지가 한국이므로 무료 구간 없이 $0.19/GiB입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;엣지 위치를 우리가 고를 수 있는 것도 아닙니다.
Netlify는 PoP 목록을 공식적으로 공개하지 않고, 지원 포럼에서 안내한 일반 CDN 지역에는 동아시아가 없습니다.
PoP가 더 많은 High-Performance Edge는 Enterprise 플랜 기능이고, 리전을 지정하는 설정은 Functions에만 있습니다(Pro 이상).
앞의 측정에서 Netlify 경유 경로의 TCP 완료가 71.7ms로 길었던 것도 이 위치와 무관하지 않아 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;응답 압축이 사라졌습니다.&lt;/strong&gt;
이쪽이 단가 변화보다 영향이 컸습니다.
전환 직후 두 경로의 헤더를 비교해 보니 이렇게 나왔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;# 전환 직후: Cloud Run 직접 매핑
$ curl -H &apos;Accept-Encoding: gzip, br&apos; -D - https://til.benelog.net/static/htmx.min.js
server: Google Frontend
content-length: 50917          # content-encoding 헤더가 없다

# 전환 전 경로: Netlify
$ curl -H &apos;Accept-Encoding: gzip, br&apos; -D - https://til-benelog.netlify.app/static/htmx.min.js
content-encoding: br
server: Netlify&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Netlify는 Brotli를 자동으로 적용해 줬는데, 이 서비스에서는 Google Frontend가 Cloud Run의 응답을 대신 압축해 주지 않았습니다.
메인 HTML도 &lt;code&gt;Accept-Encoding&lt;/code&gt;을 붙이든 붙이지 않든 같은 크기로 돌아왔습니다.
HTML·CSS·JavaScript는 보통 3~5배까지 줄어드는 자산이므로, 단가가 1.58배 오른 것과 별개로 실제로 나가는 바이트 자체가 늘어난 상태였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;앱에서_직접_응답을_압축하도록_수정&quot;&gt;4.6. 앱에서 직접 응답을 압축하도록 수정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;프록시가 대신 해 주던 일이므로 앱이 직접 하도록 미들웨어를 넣었습니다.
표준 라이브러리 &lt;code&gt;compress/gzip&lt;/code&gt;만 써서 순수 Go 구성은 그대로 유지했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;compress.go&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;// 이미 압축된 응답이나 본문이 없는 응답은 그대로 통과시킨다.
alreadyEncoded := h.Get(&quot;Content-Encoding&quot;) != &quot;&quot;
noBody := w.status == http.StatusNoContent || w.status == http.StatusNotModified

if alreadyEncoded || noBody || len(w.buf) &amp;lt; minCompressSize || !compressible(ct) {
	w.writeHeaderNow()
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;규칙은 네 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Accept-Encoding&lt;/code&gt;에 gzip이 있을 때만 압축합니다. 압축 여부와 무관하게 &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;은 항상 붙입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;512바이트 미만은 그대로 보냅니다. htmx가 갈아 끼우는 작은 조각은 압축하면 오히려 커집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;text/*&lt;/code&gt;와 JSON·JavaScript·SVG만 압축하고 PNG처럼 이미 압축된 형식은 제외합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;이미 &lt;code&gt;Content-Encoding&lt;/code&gt;이 붙어 있으면 손대지 않습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마지막 규칙 덕분에 나중에 Netlify 프록시를 다시 앞에 두더라도 이중 압축이 되지 않습니다.
HTTP 압축은 홉마다 &lt;code&gt;Accept-Encoding&lt;/code&gt;과 &lt;code&gt;Content-Encoding&lt;/code&gt;으로 협상하는 구조라, 프록시는 이미 인코딩된 응답을 다시 압축하지 않고 그대로 전달합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포한 뒤 운영 주소에서 잰 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;응답&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;압축 전&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;gzip 적용 후&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;비율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;GET /&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2,918 B&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1,220 B&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2.4배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;/static/style.css&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;4,606 B&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1,610 B&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2.9배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;/static/htmx.min.js&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;50,917 B&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;16,435 B&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;3.1배&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;압축을 해제한 결과가 원본과 바이트 단위로 같은지, PNG는 그대로 나가는지도 함께 확인했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;압축은 이렇게 되찾았지만, 목적지가 한국이라 단가가 $0.19/GiB이고 무료 구간이 없다는 조건은 그대로 남습니다.
Netlify 프록시로 되돌리면 그 두 가지는 다시 유리해지므로 선택지로 열어 두고 있습니다.
Turso로 오가는 양이 예상보다 크면 리전 구성을 다시 조정하는 것도 함께 검토할 생각입니다.
다만 지금은 청구서에 잡힌 값이 없어서 판단할 근거가 없고, 직접 매핑을 실제로 운영해 보는 것 자체가 목적이기도 해서 당분간은 이 구성을 그대로 두겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전송 요금에서 단가와 무료 한도는 기준이 서로 다릅니다.
단가는 위 표처럼 목적지가 기준인데, 무료 1GB는 &lt;strong&gt;출발지&lt;/strong&gt;에 붙습니다.
Google Cloud Free Program 문서의 Cloud Run 항목은 &quot;1 GB of outbound data transfer &lt;strong&gt;from North America&lt;/strong&gt; per month&quot;라고 적고 있습니다.
북미에서 나가는 전송에 주어지는 할당이므로, 도쿄에서 출발하는 이 앱에는 적용되지 않습니다.
Cloud Run 요금 문서는 같은 내용을 &quot;within North America&quot;로 적어 두어 표현이 조금 다르고, 네트워킹 요금표에는 목적지별로 0~1GiB 무료 구간이 표시됩니다.
문서마다 표현이 갈리는 영역이라 실제 적용 여부는 청구서로 확인하는 편이 안전하고, &lt;strong&gt;한국 목적지에는 무료 구간이 없다&lt;/strong&gt;는 쪽만 확실한 조건으로 잡았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 1GB를 받으려고 앱과 데이터베이스를 북미로 옮기는 선택은 하지 않았습니다.
월 1GB는 $0.19어치이고 초과분 단가는 그대로인데, 앞의 Turso 리전 실험에서 보듯 태평양을 한 번 건널 때마다 160ms 안팎이 붙습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또 하나 주의할 점은 이 무료 한도들이 대부분 &lt;strong&gt;결제 계정 단위&lt;/strong&gt;라는 것입니다.
같은 결제 계정에 다른 프로젝트를 붙여 쓰고 있다면 그쪽 사용량이 이 앱의 무료 한도를 함께 소모합니다.
그래서 금액이 0원이더라도 예산 알림은 걸어 두는 편이 안전합니다.
현재 상태는 Google Cloud 콘솔의 &lt;strong&gt;결제 → 보고서&lt;/strong&gt;에서 서비스별로 확인할 수 있고, &lt;strong&gt;결제 → 예산 및 알림&lt;/strong&gt;에서 임계값을 설정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;응답_시간을_좌우한_리전과_프록시&quot;&gt;5. 응답 시간을 좌우한 리전과 프록시&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run에서 배포할 리전을 서울로 골라도 데이터베이스와 프록시의 위치가 멀어지면 그 이득이 사라집니다.
두 구간을 실제 측정한 결과를 정리합니다.
아래 값은 정밀한 벤치마크의 백분위가 아니라, 한국의 같은 클라이언트에서 조건을 바꾸어 반복 호출했을 때 관찰한 범위입니다.
Cloud Run의 콜드 스타트, 인터넷 경로 변화, 연결 재사용을 모두 분리하지는 않았으므로 절대값보다 전후 차이의 방향을 보는 용도입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;오하이오에서_도쿄로_옮긴_turso_리전&quot;&gt;5.1. 오하이오에서 도쿄로 옮긴 Turso 리전&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에는 데이터베이스를 &lt;code&gt;aws-us-east-2&lt;/code&gt;(오하이오)에 두고 있었습니다.
앱을 서울 리전에 올려도 이 상태면 요청마다 태평양을 건너게 됩니다.
이 앱은 페이지 한 장을 그릴 때 질의를 한 번 보냅니다.
그룹 위치 외의 조건을 그대로 두고 응답 시간이 크게 달라졌으므로 원격 데이터베이스까지의 왕복 경로가 주요 요인이라고 추정했습니다.
서버 내부 구간별 시간을 따로 기록하지는 않았으므로 데이터베이스가 차지한 비율까지 계산할 수는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그룹을 도쿄로 옮기고 같은 조건에서 다시 측정했습니다.
관찰한 범위에서는 응답 시간이 5배 이상 차이가 났습니다.
이렇듯 애플리케이션 리전과 데이터베이스의 리전을 가까운 곳으로 지정하는 이득이 큽니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 66.6666%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Turso 그룹 위치&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;한국에서 &lt;code&gt;GET /&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;aws-us-east-2&lt;/code&gt; (오하이오)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;197 ~ 220ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;aws-ap-northeast-1&lt;/code&gt; (도쿄)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;38 ~ 45ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그룹은 데이터베이스들이 primary 리전과 서버를 공유하는 배치 단위라서, 무료 요금제의 데이터베이스는 모두 같은 리전 구성에 놓입니다.
스키마는 데이터베이스마다 독립이므로 용도가 다른 데이터베이스를 한 그룹에 함께 둘 수 있습니다.
그룹의 primary 위치는 나중에 바꿀 수 없어서, 다른 위치의 그룹으로 데이터를 옮겨야 합니다.
공식 문서상 무료(starter) 요금제는 그룹이 하나로 제한되고, 그룹을 둘 이상 만드는 기능은 유료 요금제 대상입니다.
따라서 기존 그룹과 새 그룹을 동시에 두는 이전 절차가 무료 계정에서 항상 가능하다고 가정해서는 안 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 옮길 때는 사용량 표시가 &lt;code&gt;groups 0/1&lt;/code&gt;이었는데도 두 번째 그룹이 생성됐습니다.
이 동작은 제한 적용 지연이나 계정 상태에 따른 예외일 수 있고 재현을 보장할 수 없습니다.
데이터를 보존한 채 확실하게 옮겨야 한다면 두 그룹을 동시에 쓸 수 있는 요금제로 잠시 올리거나 Turso 지원 채널에 이전 방법을 먼저 확인하는 편이 안전합니다.
기존 데이터베이스는 새 데이터베이스의 행 수와 주요 데이터를 검증하기 전에는 삭제하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;옮기는 과정에서 확인된 사실을 적어 둡니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;turso db create &amp;lt;새 이름&amp;gt; --group &amp;lt;새 그룹&amp;gt; --from-db &amp;lt;기존 DB&amp;gt;&lt;/code&gt;는 그룹이 다르면 &lt;code&gt;record not found&lt;/code&gt;로 실패합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--from-dump&lt;/code&gt;는 데이터베이스만 만들어지고 덤프가 적재되지 않은 채 종료된 경우가 있었습니다. 데이터베이스가 생성됐다는 사실만 믿지 않고 &lt;code&gt;select count(*) from entries&lt;/code&gt;처럼 테이블별 행 수를 확인해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이 이전에서는 &lt;code&gt;turso db shell &amp;lt;새 DB&amp;gt; &amp;lt; dump.sql&lt;/code&gt;로 직접 넣는 방법이 성공했습니다. &lt;code&gt;sqlite_sequence&lt;/code&gt;와 인덱스도 함께 들어갔습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;새 데이터베이스에는 토큰을 새로 발급해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;netlify_프록시와_cloud_run_직접_도메인_매핑&quot;&gt;5.2. Netlify 프록시와 Cloud Run 직접 도메인 매핑&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run 서비스는 서울에 있지만 Cloud Run의 직접 커스텀 도메인 매핑은 &lt;code&gt;asia-northeast3&lt;/code&gt;을 지원하지 않습니다.
Google이 권장하는 대안은 외부 Application Load Balancer이고 Firebase Hosting도 선택할 수 있지만, 둘 다 별도의 구성과 비용 조건을 검토해야 합니다.
이미 benelog.net의 DNS와 TLS 인증서를 Netlify에서 관리하고 있었기 때문에, til.benelog.net도 Netlify에 두고 모든 요청을 백엔드로 넘기는 rewrite 한 줄로 붙였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;proxy/_redirects&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;/*  https://til-788537242173.asia-northeast3.run.app/:splat  200&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;상태 코드 &lt;code&gt;200&lt;/code&gt;은 리다이렉트가 아니라 rewrite라서 주소창은 til.benelog.net으로 유지됩니다.
DNS와 TLS 인증서는 Netlify가 관리하므로 백엔드를 어디로 옮겨도 이 한 줄만 고치면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 프록시가 더하는 시간을 직접 확인하려고 커스텀 도메인 매핑을 지원하는 도쿄(&lt;code&gt;asia-northeast1&lt;/code&gt;)에 같은 이미지와 환경변수로 &lt;code&gt;til-tokyo&lt;/code&gt; 서비스를 만들었습니다.
Google Search Console에서 benelog.net 소유권을 확인하고 &lt;code&gt;til-tokyo.benelog.net&lt;/code&gt;의 CNAME을 &lt;code&gt;ghs.googlehosted.com.&lt;/code&gt;으로 지정했습니다.
Cloud Run 관리형 인증서는 도메인 매핑을 만든 뒤 약 57분 만에 준비됐습니다.
인증서 SAN에 &lt;code&gt;til-tokyo.benelog.net&lt;/code&gt;이 포함됐고, &lt;code&gt;/&lt;/code&gt;와 &lt;code&gt;/health&lt;/code&gt;는 모두 200으로 응답했습니다.
앞에서 설명한 &lt;code&gt;/healthz&lt;/code&gt;는 커스텀 도메인에서도 Google Frontend의 404를 반환했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 8월 22일 19시 42분(KST), 아직 운영 경로가 Netlify 프록시이던 시점에 한국의 같은 머신에서 네 주소를 측정했습니다.
각 주소를 한 번 워밍업한 뒤 20회씩 호출했고, 한 주소를 몰아서 재지 않고 매 반복마다 선두 주소를 바꿨습니다.
요청마다 새 &lt;code&gt;curl&lt;/code&gt; 프로세스를 사용했으므로 TCP와 TLS 연결도 매번 새로 맺었습니다.
모든 요청의 상태 코드는 200이었습니다.
DNS·TCP·TLS·TTFB는 요청을 시작한 시점부터 해당 단계까지 걸린 누적 시간이고, p95는 20개 중 19번째 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 22.2222%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 22.2223%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;경로&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;DNS 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;TCP 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;TLS 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;TTFB 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;전체 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;전체 최소 / p95 / 최대&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서울 &lt;code&gt;run.app&lt;/code&gt; 직접 호출&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.7ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;6.1ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;48.6ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;99.1ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;99.7ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;94.7 / 108.3 / 123.6ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도쿄 &lt;code&gt;run.app&lt;/code&gt; 직접 호출&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.8ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;6.0ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;47.7ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;97.8ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;98.8ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;93.1 / 104.2 / 114.7ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도쿄 서비스 + Cloud Run 도메인 매핑&lt;br&gt;
(CNAME → &lt;code&gt;ghs.googlehosted.com.&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.5ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;62.9ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;148.4ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;281.9ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;284.1ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;265.2 / 323.0 / 339.0ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서울 서비스 + Netlify 프록시&lt;br&gt;
(당시 운영 경로)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.3ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;71.7ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;153.9ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;347.0ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;347.1ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;335.1 / 403.1 / 413.9ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서울과 도쿄 &lt;code&gt;run.app&lt;/code&gt;의 전체 중앙값 차이는 0.9ms이고 두 표본의 변동 폭보다 작습니다.
이 결과로 어느 리전이 더 빠르다고 단정할 수 없습니다.
도메인 매핑을 거친 경로는 같은 도쿄 서비스의 &lt;code&gt;run.app&lt;/code&gt;보다 전체 중앙값이 185.3ms 길었습니다.
Cloud Run의 커스텀 도메인 진입 경로가 더한 비용을 같은 원본 서비스끼리 비교한 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;도메인 매핑 경로는 Netlify 프록시 경로보다 63.0ms 빨랐지만 &lt;code&gt;run.app&lt;/code&gt; 수준으로 짧아지지는 않았습니다.
Netlify 경유 주소의 TCP 완료가 서울 &lt;code&gt;run.app&lt;/code&gt;보다 약 66ms 늦고, 원본 서버의 응답을 다시 전달받는 구간까지 TTFB에 포함됩니다.
이 값만으로 Netlify 접속 지점의 물리적 위치를 확정할 수는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 Turso 리전 실험과 이 프록시 실험은 기초 응답 시간이 서로 다릅니다.
따라서 두 실험에서 구한 차이를 직접 더해 서로 상쇄됐다고 표현할 수는 없습니다.
다만 두 실험 모두 앱과 DB를 가까이 두는 것만큼 사용자가 접속하는 프록시 경로도 중요하다는 방향은 일치합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;커스텀 도메인을 직접 붙일 수 없는 배포처를 쓸 때는 유용한 방법이지만, 지연이 중요하면 배포처가 해당 리전에서 제공하는 도메인 연결 방법을 먼저 확인하는 편이 낫습니다.
Cloud Run 직접 도메인 매핑은 Preview 기능이고 Google도 프로덕션 사용을 권장하지 않습니다.
운영 도메인을 옮기려면 새 매핑의 인증서가 발급되는 동안 TLS 오류가 날 수 있다는 점도 고려해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;운영_도메인_전환_절차와_중단_시간&quot;&gt;5.3. 운영 도메인 전환 절차와 중단 시간&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;측정 결과를 바탕으로 도쿄 Cloud Run과 직접 도메인 매핑으로 운영 경로를 옮겼습니다.
서울과 도쿄의 직접 응답은 사실상 같았지만, 직접 매핑은 Netlify 경로보다 63ms 짧았습니다.
개인 앱에서 지연을 줄이는 쪽을 우선해 Preview 제약을 감수한 선택입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전환에서 가장 신경 쓴 부분은 중단 시간입니다.
관리형 인증서는 그 호스트의 DNS가 &lt;code&gt;ghs.googlehosted.com.&lt;/code&gt;을 가리킨 뒤에 발급되기 시작합니다.
같은 이름의 인증서를 미리 만들어 둘 방법이 없으므로, DNS를 바꾼 시점부터 인증서가 준비될 때까지는 그 주소로 접속한 사용자가 TLS 오류를 봅니다.
그래서 순서를 이렇게 잡았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;도쿄 서비스와 기존 운영 주소가 모두 정상인지 먼저 확인합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Netlify DNS에서 &lt;code&gt;til.benelog.net&lt;/code&gt;과 이름이 정확히 같은 레코드의 Type/Name/Value/TTL을 기록합니다. 이 값이 롤백 원본입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DNS를 바꾸기 전에&lt;/strong&gt; Cloud Run 매핑을 먼저 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;기존 레코드를 지우고 곧바로 &lt;code&gt;til&lt;/code&gt; CNAME 하나를 추가합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;매핑 상태와 HTTPS를 함께 관찰합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실제로 걸린 시간은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;시각 (KST)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;19:57:31&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cloud Run에 &lt;code&gt;til.benelog.net&lt;/code&gt; → &lt;code&gt;til-tokyo&lt;/code&gt; 매핑 생성. 이 시점에는 DNS가 아직 Netlify를 가리켜 &lt;code&gt;CertificatePending&lt;/code&gt;입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;20:02:30&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Netlify DNS에서 &lt;code&gt;til.benelog.net&lt;/code&gt;의 &lt;code&gt;NETLIFY&lt;/code&gt;, &lt;code&gt;NETLIFYv6&lt;/code&gt; 레코드 삭제&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;20:06:57&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;til&lt;/code&gt; CNAME &lt;code&gt;ghs.googlehosted.com.&lt;/code&gt; 추가. 권한 네임서버와 8.8.8.8에서 즉시 새 값이 조회됐습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;20:14:45 이전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;매핑이 &lt;code&gt;Ready=True&lt;/code&gt;, &lt;code&gt;CertificateProvisioned=True&lt;/code&gt;로 전환&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;20:22:59&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;&lt;a href=&quot;https://til.benelog.net/&quot; class=&quot;bare&quot;&gt;https://til.benelog.net/&lt;/a&gt;&lt;/code&gt;이 처음으로 200 응답&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;레코드를 지운 20:02:30부터 HTTPS가 정상화된 20:22:59까지 약 20분 30초 동안 이 주소는 정상 동작하지 않았습니다.
그중 약 4분 30초는 레코드를 지운 뒤 CNAME을 추가하기 전까지 이름 자체가 응답하지 않은 구간이고, 나머지는 TLS 오류 구간입니다.
기존 레코드의 TTL이 120초였으므로 삭제 직후 잠시 옛 주소로 접속한 사용자도 있었을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인증서 자체는 CNAME을 넣은 지 8분 이내에 준비됐습니다.
앞서 테스트 호스트 &lt;code&gt;til-tokyo.benelog.net&lt;/code&gt;에서는 같은 단계에 약 57분이 걸렸으므로, 이 시간은 호스트마다 다르다고 봐야 합니다.
Google 안내상 최대 24시간이 걸릴 수 있습니다.
&lt;code&gt;CertificateProvisioned=True&lt;/code&gt;가 된 뒤에도 8분 정도 TLS 오류가 이어졌습니다.
매핑 상태가 준비됐다는 것과 Google Frontend가 그 인증서를 실제로 제공하기 시작하는 것은 별개의 시점입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전환 뒤 발급된 인증서는 subject가 &lt;code&gt;CN = til.benelog.net&lt;/code&gt;, issuer가 &lt;code&gt;Google Trust Services, CN = WR3&lt;/code&gt;이고 유효 기간은 2026년 8월 22일부터 11월 20일까지입니다.
SAN에는 &lt;code&gt;til.benelog.net&lt;/code&gt; 하나만 들어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전환 직후 같은 방식으로 다시 측정한 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 22.2222%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 11.1111%;&quot;&gt;
&lt;col style=&quot;width: 22.2223%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;경로&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;DNS 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;TCP 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;TLS 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;TTFB 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;전체 중앙값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;전체 최소 / p95 / 최대&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도쿄 &lt;code&gt;run.app&lt;/code&gt; 직접 호출&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.3ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;5.7ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;46.7ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;97.9ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;98.5ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;92.4 / 104.6 / 141.8ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도쿄 서비스 + Cloud Run 도메인 매핑&lt;br&gt;
(전환 후 운영 경로)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1.3ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;54.2ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;116.0ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;223.2ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;224.2ms&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;210.4 / 240.8 / 255.3ms&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전환 전 Netlify 프록시 경로의 347.1ms와 비교하면 전체 중앙값이 122.9ms 짧아졌습니다.
다만 이 값은 전환 약 20분 뒤에 잰 것이라 앞의 측정과 시간대가 다릅니다.
같은 시점에 잰 &lt;code&gt;run.app&lt;/code&gt;이 98.5ms이므로, 커스텀 도메인 진입 경로가 더하는 비용은 여전히 125.7ms입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서울 서비스 &lt;code&gt;til&lt;/code&gt;, Netlify 사이트의 &lt;code&gt;proxy/_redirects&lt;/code&gt;, 기존 DNS 레코드의 값은 지우지 않고 롤백 경로로 남겼습니다.
DNS를 원래 레코드로 되돌리면 프록시 경로가 그대로 다시 동작합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;선택하지_않은_배포처&quot;&gt;6. 선택하지 않은 배포처&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cloud Run으로 정하기 전에 처음 세운 조건은 &quot;카드 등록 없이 상시 가동&quot;이었습니다.
여기서 상시 가동은 프로세스가 항상 메모리에 떠 있다는 뜻이 아니라, 사용자가 요청하면 같은 URL로 다시 접근할 수 있다는 뜻입니다.
다만 유휴 상태에서 깨어나는 데 수십 초가 걸리면 개인 도구로도 불편하다고 판단했습니다.
그 조건으로 찾아본 후보와, 각각을 접은 이유를 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;후보&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;무료 조건&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;걸린 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Back4app Containers&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;컨테이너 1개, 256MB RAM / 0.25 CPU&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;카드가 필요 없습니다. 다만 제가 확인한 배포에서는 무료 주소가 만료될 때 배포도 함께 삭제됐습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Render&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Free web service, workspace당 월 750시간&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15분 동안 요청이 없으면 컨테이너가 중지되고, 복귀에 30초에서 1분이 걸립니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Hugging Face Spaces&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;CPU Basic, 공개 Space 한정&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;유휴 시 중지됩니다. 데이터베이스 이름과 화면이 공개됩니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Northflank&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;프로젝트 1개, 서비스 2개&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;리소스를 생성할 때 카드 등록을 요구합니다. 무료 리전도 us-central과 europe-west뿐입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Fly.io, Railway, Koyeb, Heroku&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;상시 운영 가능한 무료 컨테이너 요금제 없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;카드 등록 여부를 더 비교하기 전에 무료 상시 운영 조건에서 제외했습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Northflank는 카드 요구로 먼저 제외했고, 다음으로 Back4app Containers를 골랐습니다.
카드 없이 GitHub 저장소를 연결하면 Kaniko가 Dockerfile을 빌드해 배포하고, 컨테이너가 중지되지 않아 콜드스타트도 관측되지 않았습니다.
여기까지는 조건에 맞았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;back4app_containers의_임시_url_만료&quot;&gt;6.1. Back4app Containers의 임시 URL 만료&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포한 지 30분이 되지 않아 til.benelog.net이 응답하지 않았습니다.
대시보드의 배포 로그에 원인이 그대로 남아 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Back4app Containers의 배포 로그 (UTC)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;2026-08-21T23:58:26Z  SYSTEM  LAUNCHING CONTAINER...
2026-08-21T23:59:19Z  SYSTEM  CHECKING HEALTH...
2026-08-21T23:59:20Z  SYSTEM  DEPLOYMENT READY
2026-08-22T00:27:12Z  SYSTEM  The Back4app custom domain has expired for free plan
2026-08-22T00:27:12Z  SYSTEM  COOLING DOWN...
2026-08-22T00:28:12Z  SYSTEM  FINISHING CONTAINER...
2026-08-22T00:28:12Z  SYSTEM  DEPLOYMENT DESTROYED&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컨테이너가 고장난 것이 아니라 &lt;strong&gt;무료 요금제의 임시 주소가 만료되면서 배포 자체가 삭제&lt;/strong&gt;됐습니다.
그 사이 서비스는 정상이었고, 앱 로그에 &lt;code&gt;GET /&lt;/code&gt; 요청이 200으로 남아 있습니다.
대시보드에는 &quot;URL is temporary and will be live for 60 minutes&quot;라는 안내가 있는데, 공식 문서에는 설명이 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;만료 시점은 재현하지 못했습니다.
위 사례는 &lt;code&gt;DEPLOYMENT READY&lt;/code&gt; 이후 약 28분이고, 다른 때는 65분이 지나도 같은 주소가 살아 있었습니다.
따라서 안내문의 60분을 정확한 만료 시점이나 최소 보장 시간으로 해석할 수 없었습니다.
무료 주소의 수명을 운영 계약처럼 믿을 수 없다는 점만 결론으로 삼았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;진단이 어려웠던 이유는 따로 있습니다.
만료된 주소가 &lt;strong&gt;한 번도 존재한 적 없는 서브도메인과 똑같은 응답&lt;/strong&gt;을 돌려줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl -sI https://til-nsh62xv6.b4a.run/healthz   # 실제로 만들었던 앱
curl -sI https://til-zzzzzzzz.b4a.run/healthz   # 존재한 적 없는 이름
# 둘 다: HTTP/2 404, &quot;not found&quot;, x-cache: Error from cloudfront&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컨테이너 장애 때 502나 503이 흔하지만, 플랫폼이 라우팅을 먼저 제거하면 404를 돌려줄 수도 있습니다.
이 404와 헤더만으로는 앱 장애인지 주소 회수인지 구분할 수 없었습니다.
대시보드의 배포 로그를 봐야 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기에 재배포하면 주소 접미사가 바뀌는 문제가 더해집니다.
실제로 되살릴 때마다 &lt;code&gt;til-nsh62xv6&lt;/code&gt;에서 &lt;code&gt;til-xu9uffq7&lt;/code&gt;로 바뀌었고, 그때마다 Netlify의 &lt;code&gt;_redirects&lt;/code&gt;를 고쳐야 했습니다.
잠깐 띄워 보여 주는 용도라면 쓸 만하지만, 주소가 항상 살아 있어야 한다는 조건에는 맞지 않았습니다.
영구 주소는 월 5달러 요금제부터입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 &quot;카드 등록 없이&quot;라는 조건을 포기하고 Cloud Run으로 옮겼습니다.
카드를 등록해야 한다는 조건이 같다면, 무료 리전이 us-central과 europe-west뿐인 Northflank보다 서울 리전에서 요청이 없을 때 0개로 축소되는 Cloud Run이 이 앱의 사용 방식에 더 잘 맞았습니다.
GitHub 연동으로 푸시할 때마다 자동 배포되던 점은 Back4app 쪽이 편했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;이_구성의_제약&quot;&gt;7. 이 구성의 제약&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 방식을 검토하실 분을 위해 제약과 단점을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloud Run은 무료 한도만 써도 결제 계정 연결이 필수입니다.&lt;/strong&gt; 카드를 등록하지 않겠다는 조건이면 이 선택지는 빠집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloud Run 서비스 사용량 외의 비용 항목이 있습니다.&lt;/strong&gt; 특히 한국으로 나가는 응답에는 무료 구간이 없습니다. 항목별 조건은 앞의 &quot;Cloud Run 요금이 0원을 넘어가는 조건&quot; 섹션에 정리했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;요청이 없으면 인스턴스가 0개로 줄어듭니다.&lt;/strong&gt; URL은 유지되지만 다음 첫 요청에는 콜드 스타트가 포함될 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;자동 배포는 따로 설정해야 합니다.&lt;/strong&gt; &lt;code&gt;gcloud run deploy&lt;/code&gt;를 직접 실행하는 방식이라, 푸시할 때마다 배포되게 하려면 Cloud Build 트리거를 붙여야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;운영 도메인이 Preview 기능에 의존합니다.&lt;/strong&gt; Cloud Run의 직접 커스텀 도메인 매핑은 Preview이고 Google도 프로덕션 사용을 권장하지 않습니다. 서울 리전에서는 아예 지원하지 않아 서비스를 도쿄로 옮겨야 했습니다. 직접 매핑과 Netlify 프록시가 응답 시간에 더한 비용은 앞의 &quot;응답 시간을 좌우한 리전과 프록시&quot; 섹션에서 측정했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;도메인 연결 방식을 바꾸는 동안 서비스가 중단됩니다.&lt;/strong&gt; 같은 호스트 이름의 관리형 인증서를 미리 준비해 둘 수 없기 때문인데, 실제 전환에서 관찰한 중단 시간은 앞의 &quot;운영 도메인 전환 절차와 중단 시간&quot; 섹션에 적었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Turso 무료 요금제의 그룹 위치는 나중에 바꿀 수 없고, 공식적으로 그룹 하나만 쓸 수 있습니다.&lt;/strong&gt; 처음 만들 때 앱이 뜰 리전에 맞추는 편이 좋습니다. 다른 리전으로 옮기는 절차의 제약은 앞의 &quot;오하이오에서 도쿄로 옮긴 Turso 리전&quot; 섹션의 참고 블록에 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포처를 옮기는 동안 저장소 접근 코드와 Dockerfile은 한 줄도 바꾸지 않았습니다.
Turso가 SQLite와 호환되고 이미지가 표준 컨테이너라서 &lt;strong&gt;애플리케이션 코드의 이식 비용&lt;/strong&gt;은 환경변수와 배포 설정을 바꾸는 정도에 그쳤습니다.
계정 생성, IAM 설정, 데이터 이전, 도메인 전환 같은 운영 작업까지 몇 줄로 끝났다는 뜻은 아닙니다.
무료 요금제의 조건이 자주 바뀌는 영역에서는 이렇게 옮기기 쉬운 구성이 선택 자체보다 중요하다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;8. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/til&quot;&gt;benelog/til&lt;/a&gt; — 이 글에서 설명한 저장소&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/run/pricing&quot;&gt;Cloud Run 요금&lt;/a&gt; — 무료 한도와 리전별 단가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/run/docs/overview/what-is-cloud-run&quot;&gt;Cloud Run 개요&lt;/a&gt; — 0개로 축소되는 과금 방식과 휘발성 컨테이너 파일 시스템&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/run/docs/configuring/services/build-service-account&quot;&gt;Cloud Run: 빌드 서비스 계정&lt;/a&gt; — &lt;code&gt;PERMISSION_DENIED&lt;/code&gt;가 나올 때 확인할 문서&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/build/pricing&quot;&gt;Cloud Build 요금&lt;/a&gt; — 소스 빌드의 별도 무료 한도와 초과 요금&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/artifact-registry/pricing&quot;&gt;Artifact Registry 요금&lt;/a&gt; — 소스 배포 이미지의 별도 저장 비용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/run/docs/mapping-custom-domains&quot;&gt;Cloud Run: 커스텀 도메인 매핑&lt;/a&gt; — 서울 리전의 직접 매핑 미지원과 대안&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/run/docs/known-issues&quot;&gt;Cloud Run: Known issues&lt;/a&gt; — 예약된 URL 경로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/run/docs/mapping-custom-domains#adding_verified_domain&quot;&gt;Cloud Run: 도메인 소유권 확인&lt;/a&gt; — 매핑 전에 필요한 Search Console 검증&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/vpc/network-pricing&quot;&gt;Google Cloud 네트워킹 요금&lt;/a&gt; — Cloud Run에서 나가는 외부 전송에 적용되는 단가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/free/docs/free-cloud-features&quot;&gt;Google Cloud Free Program: Always Free&lt;/a&gt; — 서비스별 상시 무료 한도. Cloud Run의 전송 무료 1GB가 북미 출발 기준이라고 적힌 곳&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.turso.tech/cli/group&quot;&gt;Turso CLI: group&lt;/a&gt; — 그룹 생성과 리전 지정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.turso.tech/cli/group/create&quot;&gt;Turso CLI: group create&lt;/a&gt; — 여러 그룹을 만들 수 있는 요금제 제한&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://turso.tech/pricing&quot;&gt;Turso 요금&lt;/a&gt; — 무료 요금제의 한도&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.netlify.com/routing/redirects/rewrites-proxies/&quot;&gt;Netlify: Rewrites and proxies&lt;/a&gt; — 상태 코드 200 rewrite&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.google.com/blog/topics/developers-practitioners/health-checking-your-grpc-servers-gke&quot;&gt;Health checking your gRPC servers on GKE&lt;/a&gt; — Google 내부 z-pages 관례가 언급된 글&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/streamlit/streamlit/issues/3028&quot;&gt;Streamlit: Change health check endpoint name&lt;/a&gt; — &lt;code&gt;/healthz&lt;/code&gt;가 GCP 예약 경로라 겪은 같은 문제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.back4app.com/pricing/container-as-a-service&quot;&gt;Back4app Containers 요금&lt;/a&gt; — 무료 요금제의 조건&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;dolthub-app.html&quot;&gt;DoltHub에 데이터를 저장하는 애플리케이션 개발&lt;/a&gt; — 같은 시기에 만든 앱. 데이터를 사용자 계정에 두는 반대 방향의 선택&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code, Codex와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>DoltHub에 데이터를 저장하는 애플리케이션 개발</title>
      <link>https://blog.benelog.net//dolthub-app.html</link>
      <pubDate>Sat, 22 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">dolthub-app.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#습관_추적기의_화면과_연결_절차&quot;&gt;1. 습관 추적기의 화면과 연결 절차&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#데이터_저장소_후보_비교와_dolthub_선택&quot;&gt;2. 데이터 저장소 후보 비교와 DoltHub 선택&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#토큰을_저장하지_않고_기본_db도_두지_않는_애플리케이션_서버_구성&quot;&gt;3. 토큰을 저장하지 않고 기본 DB도 두지 않는 애플리케이션 서버 구성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#htmx_서버_렌더링과_cloudflare_pages_배포&quot;&gt;4. htmx 서버 렌더링과 Cloudflare Pages 배포&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dolthub_api를_사용한_스키마_자동_생성&quot;&gt;5. DoltHub API를 사용한 스키마 자동 생성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dolthub_저장_구성의_제약&quot;&gt;6. DoltHub 저장 구성의 제약&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;7. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;don&amp;#8217;t break the chain&quot; 방식의 습관 추적기를 만들었습니다.
웹 페이지의 달력에서 하루 칸을 클릭하면 사슬이 이어지고, 하루를 건너뛰면 사슬이 끊어지는 단순한 기능의 앱입니다.
이런 도구는 이미 많지만, 데이터를 서비스 운영자의 DB가 아니라 사용자가 소유하고 통째로 가져갈 수 있는 곳에 저장하면서 무료로 운영하고 싶었습니다. 그래서 사용자가 직접 DB를 생성할 수 있는 &lt;strong&gt;&lt;a href=&quot;https://www.dolthub.com/&quot;&gt;DoltHub&lt;/a&gt;&lt;/strong&gt;를 데이터 저장소로 사용했습니다.
이 글에서는 그 구성의 선택지와 제약, 구현하면서 확인한 사실을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소스는 &lt;a href=&quot;https://github.com/benelog/habit-chain&quot;&gt;benelog/habit-chain&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;습관_추적기의_화면과_연결_절차&quot;&gt;1. 습관 추적기의 화면과 연결 절차&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;화면은 습관마다 카드 하나입니다. 카드 안에는 최근 5주를 요일별로 늘어놓은 격자가 있고, 체크한 날끼리는 서로 이어져 하나의 사슬로 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/habit-chain/habit-chain-list.jpg&quot; alt=&quot;습관 카드 하나에 최근 5주 격자가 있고 7월 22일부터 25일까지 네 칸이 이어져 있다&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;사용자는 &lt;a href=&quot;https://www.dolthub.com/&quot;&gt;DoltHub&lt;/a&gt;에 가입하고 DB를 생성해야 합니다. 공개 DB는 이름만으로 읽을 수 있고, 비공개 DB는 읽기에도 토큰이 필요합니다. 앱은 설정된 토큰을 읽기 요청에도 전달하므로 둘 다 지원합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;DoltHub에서 빈 데이터베이스를 하나 만듭니다. 테이블은 만들지 않아도 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DoltHub의 &lt;code&gt;Settings → Tokens&lt;/code&gt;에서 API 토큰을 발급합니다. 토큰은 가능한 한 이 DB와 필요한 권한으로만 제한합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;앱의 설정 화면에 데이터베이스 이름(&lt;code&gt;owner/name&lt;/code&gt;)과 토큰을 넣고 저장합니다. 저장하는 시점에 앱이 DoltHub에 토큰이 유효한지 물어보므로, 잘못 붙여 넣은 토큰은 그 자리에서 드러납니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;목록 자리에 나타나는 &lt;strong&gt;DB 준비하기&lt;/strong&gt; 버튼을 누릅니다. 앱이 사용자의 토큰으로 테이블을 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;4단계가 끝나면 바로 습관을 추가할 수 있습니다. 스키마 SQL을 손으로 옮겨 붙이는 단계는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;데이터_저장소_후보_비교와_dolthub_선택&quot;&gt;2. 데이터 저장소 후보 비교와 DoltHub 선택&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;데이터 저장소는 아래의 조건을 고려해서 선택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;제가 서버 비용과 백업 책임을 지지 않아야 함&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;사용자가 자기 데이터를 언제든 통째로 가져갈 수 있어야 함&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL로 데이터 조회/입력 가능&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;선택지&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;장점&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;걸린 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cloudflare D1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Worker와 같은 계정에서 바로 쓰고 지연이 짧음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;데이터가 제 계정에 쌓입니다. 사용자별로 격리하려면 인증과 테넌트 구분을 직접 만들어야 합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;브라우저 localStorage&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서버가 아예 필요 없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기기를 바꾸면 기록이 사라집니다. 브라우저 데이터를 지워도 사라집니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;GitHub 저장소에 JSON 커밋&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용자 소유이고 이력이 남음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;체크 한 번에 파일 전체를 다시 쓰게 됩니다. 조건 검색도 직접 구현해야 합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;DoltHub&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용자 소유, SQL 질의, 커밋 이력, 웹 관리 화면&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;쓰기 한 번에 1.5초에서 2초가 걸립니다. 무료 요금제의 데이터베이스는 공개됩니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DoltHub는 Git처럼 커밋과 브랜치를 갖는 SQL 데이터베이스인 Dolt의 호스팅 서비스입니다. 사용자가 자기 계정에 데이터베이스를 만들고, 앱은 사용자가 발급한 토큰으로 그 데이터베이스에 접근합니다.
체크를 한 번 할 때마다 커밋이 하나 쌓이므로, 언제 무엇을 고쳤는지가 DoltHub의 커밋 로그에 그대로 남습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/habit-chain/dolthub-commit-log.png&quot; alt=&quot;DoltHub의 Commit Log 화면. 체크 추가·삭제와 습관 수정이 SQL 구문 단위의 커밋으로 나열되어 있다&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이력은 Git처럼 쌓이지만 조회는 SQL로 합니다. Dolt가 커밋 로그와 행 단위 변경 이력을 시스템 테이블로 노출하므로, 별도 명령을 배우지 않아도 익숙한 SQL로 이력을 들여다볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;-- 커밋 이력 조회
SELECT commit_hash, committer, date, message FROM dolt_log LIMIT 3;

-- 3개 커밋 전 시점의 체크 기록 조회
SELECT * FROM checks AS OF &apos;HEAD~3&apos;;

-- checks 테이블의 행 단위 변경 이력 조회
SELECT check_date, commit_date FROM dolt_history_checks ORDER BY commit_date DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;웹 화면의 데이터 관리 기능도 풍부합니다. DoltHub에서 테이블을 직접 편집하고, 커밋별 diff를 확인하고, SQL 콘솔에서 질의를 실행할 수 있습니다. 데이터베이스를 fork하고 pull request로 변경을 제안하는 흐름도 Git 호스팅과 같습니다. 덕분에 앱에 관리 화면을 따로 만들지 않아도 DoltHub 웹 화면이 그 역할을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/habit-chain/dolthub-checks-table.png&quot; alt=&quot;DoltHub의 Database 화면. 왼쪽에 테이블 목록과 스키마가 있고&quot; width=&quot;SELECT 질의 결과로 checks 테이블의 체크 기록이 나온다&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;데이터_반출과_백업의_경계&quot;&gt;2.1. 데이터 반출과 백업의 경계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앱의 설정 화면에는 현재 앱이 읽는 상태를 JSON으로 내려받는 기능이 있습니다. 이 내려받기는 일반 링크여서 URL에 비밀을 싣지 않으려고 토큰을 전달하지 않으므로, 공개 DB에서만 동작합니다. 스키마와 커밋 이력까지 포함한 DB 전체를 가져오려면 Dolt CLI로 저장소를 clone할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;dolt clone owner/name&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 앱 운영자인 제가 사용자 데이터의 백업과 반출 기능을 별도로 만들 필요는 없습니다. 다만 DoltHub의 커밋 이력은 DoltHub 계정 삭제나 서비스 장애까지 막아 주는 별도의 백업은 아닙니다. 기록이 중요하다면 사용자가 주기적으로 clone해서 다른 곳에 사본을 두어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;토큰을_저장하지_않고_기본_db도_두지_않는_애플리케이션_서버_구성&quot;&gt;3. 토큰을 저장하지 않고 기본 DB도 두지 않는 애플리케이션 서버 구성&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 구성에서 서버가 하는 일은 화면을 그리고 DoltHub API를 대신 호출하는 것뿐입니다. 서버는 다음 두 가지를 갖지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;영속적으로 저장한 DB 접근 토큰&lt;/strong&gt;: 사용자가 설정에 넣은 값이 요청마다 &lt;code&gt;X-Dolt-Token&lt;/code&gt; 헤더로 옵니다. Worker는 요청을 처리하는 동안 토큰을 취급하지만 DB나 환경 변수에 저장하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;기본 데이터베이스&lt;/strong&gt;: 설정이 비어 있는 사용자에게 보여 줄 데이터베이스가 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;공개 DB의 읽기는 토큰 없이 누구나 할 수 있고, 비공개 DB의 읽기와 모든 쓰기는 DoltHub가 토큰의 권한으로 통제합니다. Worker는 사용자 계정이나 세션을 구현하지 않고 권한 검사를 DoltHub에 맡깁니다.
사용 편의성을 위해 DB 이름과 토큰은 브라우저의 localStorage에 저장합니다. 공용 PC에는 저장하지 말 것을 안내하고, 저장한 값을 지우는 버튼도 두었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 화면의 shell은 고정된 HTML이지만, 습관 목록은 DB의 현재 내용과 브라우저가 보낸 오늘 날짜로 Worker가 동적으로 렌더링합니다. 서버에 세션이나 사용자별 저장 공간이 없다는 뜻에서 상태가 없을 뿐, 정적 페이지는 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DB 이름과 날짜, 토큰을 매 요청에 싣는 구현은 htmx에서 했습니다.
htmx의 &lt;code&gt;hx-headers&lt;/code&gt;는 상속되는 속성이라, &lt;code&gt;body&lt;/code&gt;에 한 번만 선언하면 모든 요청에 추가됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;worker/src/render.ts가 만드는 shell&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&amp;lt;body hx-headers=&apos;js:{&quot;X-Local-Date&quot;: habitChain.today(), &quot;X-Dolt-DB&quot;: habitChain.db(), &quot;X-Dolt-Token&quot;: habitChain.token()}&apos;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;js:&lt;/code&gt; 접두사를 붙이면 요청 시점마다 표현식을 평가합니다. 그래서 설정을 바꾸면 다음 요청부터 새 값이 실립니다.
이 구조에서는 읽기 요청에도 토큰이 실리고, Worker는 설정된 토큰을 DoltHub 읽기 호출에 전달합니다. 비공개 DB를 읽는 경로가 이것입니다. 부수 효과도 하나 있습니다. 2026년 8월에 확인했을 때 DoltHub의 읽기 엔드포인트는 공개 DB의 조회라도 잘못된 토큰이 붙어 있으면 400으로 거부했습니다. 그래서 저장한 뒤에 폐기된 토큰도 다음 읽기에서 드러나며, 앱은 이 오류 메시지를 알아보고 설정 화면을 가리키는 안내를 띄웁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;토큰이 앱의 여러 경로를 흐르므로 Worker 코드와 배포 경로, 같은 출처에서 실행되는 JavaScript를 신뢰해야 합니다. 특히 localStorage를 읽을 수 있는 XSS가 생기면 토큰이 앱 밖으로 유출될 수 있으므로, 토큰의 대상 DB와 권한을 최소화해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;저장_시점의_토큰_검증&quot;&gt;3.1. 저장 시점의 토큰 검증&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;잘못 붙여 넣은 토큰은 검증이 없으면 첫 쓰기에서야 오류로 드러납니다. 그래서 설정의 저장 버튼을 누르는 시점에 Worker가 DoltHub의 v2 API인 &lt;code&gt;GET /api/v2/user&lt;/code&gt;로 토큰이 유효한지 확인합니다. `v1alpha1`에는 DB와 무관하게 토큰만 확인하는 문서화된 엔드포인트가 없어서, 이 요청 하나만 v2를 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;판정은 유효와 무효의 두 가지가 아니라 세 가지로 나눴습니다. DoltHub가 명확히 거부(401, 403)한 토큰만 저장을 막습니다. DoltHub 장애나 네트워크 오류처럼 판정할 수 없는 경우에는 확인하지 못했다는 안내와 함께 저장을 진행합니다. 저장소 쪽 장애를 &quot;토큰이 틀렸다&quot;로 보여 주면 사용자가 멀쩡한 토큰을 의심하게 되기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;htmx_서버_렌더링과_cloudflare_pages_배포&quot;&gt;4. htmx 서버 렌더링과 Cloudflare Pages 배포&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;화면 렌더링은 전부 서버에서 합니다. 브라우저는 htmx가 HTML 조각을 갈아 끼우는 일만 합니다. 브라우저에서 DoltHub를 직접 호출하지 않으므로 CORS 설정에 기댈 일이 없습니다. 2026년 8월에 확인했을 때 DoltHub의 write 엔드포인트는 브라우저의 preflight 요청에 쓰기를 허용하지 않았으므로, Worker의 API 중계 역할은 서버 렌더링과 별개로도 필요했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이름과 설명을 고치는 폼은 카드마다 접힌 채로 함께 내려보냅니다. 수정 버튼을 누르면 클래스 하나만 바꿔서 펼치므로 서버를 다시 부르지 않습니다. 읽기 한 번에 1초 가까이 걸리는 앱이라, 폼을 그때 받아 오면 수정 버튼과 취소 버튼이 매번 멈춥니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/habit-chain/habit-chain-edit.jpg&quot; alt=&quot;카드 안에서 이름과 설명 입력란이 펼쳐지고 아래에는 사슬 격자가 그대로 남아 있다&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;취소는 &lt;code&gt;form.reset()&lt;/code&gt;으로 처리합니다. 서버가 내려보낸 값이 곧 폼의 기본값이라서 따로 되돌릴 값을 들고 있지 않아도 됩니다. 저장에 성공하면 서버가 카드를 통째로 새로 내려주므로 펼침 상태도 함께 사라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버 실행 환경으로 Cloudflare를 고른 이유는 Workers입니다. 관리할 서버 없이 요청 단위로 코드가 실행되고, 무료 요금제로 하루 10만 요청까지 처리할 수 있습니다. 개인용 습관 추적기의 트래픽은 그 안에 넉넉히 들어갑니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포는 Cloudflare Pages의 advanced mode를 씁니다. 빌드 산출물의 &lt;code&gt;_worker.js&lt;/code&gt; 하나가 모든 요청을 먼저 받고, 자기가 모르는 경로만 정적 자산으로 넘깁니다. 이 &lt;code&gt;_worker.js&lt;/code&gt;도 같은 Workers 런타임에서 실행되므로, Pages를 써도 실행 모델은 그대로입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데도 Workers 직접 배포가 아니라 Pages를 고른 이유는 도메인입니다. 이 앱의 도메인 DNS는 Netlify에 있는데, Workers의 커스텀 도메인은 Cloudflare 존을 요구합니다. Pages는 서브도메인이면 외부 DNS에 CNAME 한 줄로 붙습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;x_local_date_헤더로_받는_오늘_날짜&quot;&gt;4.1. X-Local-Date 헤더로 받는 오늘 날짜&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버에는 사용자의 시간대가 없습니다. UTC를 기준으로 삼으면 한국 사용자는 오전 9시 전에 체크한 기록이 어제 칸에 들어갑니다. 습관 추적기에서 그것은 사슬이 끊어진 것으로 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 브라우저가 로컬 날짜를 &lt;code&gt;X-Local-Date&lt;/code&gt; 헤더로 보내고, 서버는 그 값을 씁니다. 헤더가 없을 때만 UTC로 계산합니다. 클라이언트가 보낸 값이므로 형식은 검사합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;체크 시각을 UTC로 저장하고 조회할 때 사용자의 시간대로 변환하는 방법도 검토했지만, 이 앱에는 맞지 않다고 판단했습니다. &lt;code&gt;check_date&lt;/code&gt;는 시각이 아니라 &quot;그 날 했다&quot;는 날짜이기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;checks&lt;/code&gt; 테이블의 PK인 &lt;code&gt;(habit_id, check_date)&lt;/code&gt;가 하루에 체크 하나라는 규칙을 스키마에 담고 있습니다. 현재 스키마에서 UTC 시각만 저장하면 하루의 경계가 조회하는 시간대에 따라 달라지므로, 이 규칙을 같은 방식으로 표현할 수 없습니다. 체크 해제도 지금은 날짜 단위 &lt;code&gt;DELETE&lt;/code&gt; 구문 하나인데, 시각 저장이면 범위 질의가 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;시간대를 옮겨서 조회하면 과거 기록이 다른 날로 재해석됩니다. 밤 11시의 체크가 다음 날로 넘어가면, 이미 이어졌던 사슬이 나중에 끊어져 보입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;렌더링이 서버에서 일어나므로, 조회할 때 변환하려면 결국 브라우저가 시간대를 헤더로 보내야 합니다. &lt;code&gt;X-Local-Date&lt;/code&gt;와 같은 클라이언트 의존이 그대로 남고 변환 지점만 늘어납니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DoltHub 웹 화면에서 &lt;code&gt;2026-07-25&lt;/code&gt;라는 날짜는 그대로 읽히지만, UTC 시각은 시간대를 계산해야 맞는 날이 보입니다. 관리 화면을 DoltHub에 맡긴 구성과 어긋납니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;시각인 데이터는 UTC로 저장합니다. 습관의 생성 시점인 &lt;code&gt;created_at&lt;/code&gt; 칼럼이 그렇습니다. 남는 트레이드오프는 브라우저가 보낸 날짜를 형식만 검사하고 신뢰한다는 점입니다. 조작된 날짜를 막는 인증 경계가 아니라, 사용자가 자기 DB에 기록하는 개인 도구이므로 받아들였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dolthub_api를_사용한_스키마_자동_생성&quot;&gt;5. DoltHub API를 사용한 스키마 자동 생성&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 앱은 SQL 읽기와 쓰기에 DoltHub의 &lt;code&gt;v1alpha1&lt;/code&gt; API를 사용합니다. 2026년 7월에 명시적인 OpenAPI 계약과 일관된 오류 형식을 갖춘 v2 API가 출시되었고, 공식 문서도 새 연동에는 v2를 권장합니다. 그래서 전체 이전을 검토했지만, 2026년 8월에 확인한 아래 사실 때문에 보류했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;v2의 &lt;code&gt;/api/v2/databases/{owner}/{db}&lt;/code&gt; 계열 엔드포인트가, `v1alpha1`로는 정상으로 읽히는 살아 있는 저장소 다수에 404(&lt;code&gt;no such repository&lt;/code&gt;)를 반환했습니다. 제가 실제로 쓰는 DB와 v2 출시 이후에 만든 DB도 마찬가지였고, v2 문서의 예시 응답에 등장하는 &lt;code&gt;dolthub/us-jails&lt;/code&gt;조차 라이브 v2에서는 404였습니다. 이 격차는 마이그레이션 가이드에 언급이 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;v1alpha1`은 폐기 예정이 아닙니다. v2 발표 글이 지원 지속을 명시하고, SQL API의 본 문서도 여전히 `v1alpha1&lt;/code&gt; 기준입니다. &lt;code&gt;/api/v1&lt;/code&gt; 같은 정식 v1은 존재하지 않아서, 이름과 달리 `v1alpha1`이 현행 주력 API입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2 전용 토큰은 따로 없습니다. 같은 API 토큰을 `v1alpha1`은 &lt;code&gt;authorization: token&lt;/code&gt; 스킴으로, v2는 &lt;code&gt;Authorization: Bearer&lt;/code&gt; 스킴으로 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;커버리지 문제가 풀린 뒤에 v2로 옮기더라도 확인할 것이 남습니다. 응답 형식이 다르고(행이 칼럼명을 가진 객체가 아니라 값의 배열입니다), 아래에서 설명할 문서에 없는 write 동작이 v2의 &lt;code&gt;sql-writes&lt;/code&gt; 엔드포인트에서도 유지되는지 다시 확인해야 합니다. 따라서 아래 내용은 DoltHub API 일반의 보장이 아니라 2026년 8월에 확인한 `v1alpha1`의 동작입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DoltHub의 DDL 실행 경로는 아래 3가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 50.0001%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;경로&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;DDL&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;확인한 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;웹의 SQL 조회 콘솔&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;거부&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt;을 보내면 &lt;code&gt;Unsupported SQL statement&lt;/code&gt;로 거부합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;웹의 테이블 편집 화면&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;허용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;테이블 목록에서 연필 아이콘을 눌러 들어가는 SQL Query 화면입니다. 임시 workspace에 반영한 뒤 커밋합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;REST write 엔드포인트&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;허용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;앱이 쓰기에 쓰는 &lt;code&gt;POST /api/v1alpha1/{owner}/{db}/write/{branch}/{branch}&lt;/code&gt;입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;REST write 엔드포인트가 DDL을 받는다는 내용은 &lt;a href=&quot;https://www.dolthub.com/docs/products/dolthub/api/v1alpha1/sql&quot;&gt;공식 문서&lt;/a&gt;에 없습니다. 2026년 8월에 빈 데이터베이스로 직접 확인한 동작이므로, 보장된 계약으로 보지 않고 실패 경로를 남겨 두었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 스키마 준비를 다음과 같이 구현했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;목록 읽기가 실패하면 그때만 &lt;code&gt;SHOW TABLES&lt;/code&gt;와 &lt;code&gt;SHOW COLUMNS&lt;/code&gt;로 데이터베이스의 상태를 확인합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스키마 상태가 충분하지 않으면 화면에서 &lt;strong&gt;DB 준비하기&lt;/strong&gt; 버튼을 보여 줍니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;버튼을 누르면 서버가 상태를 다시 확인하고, 앱이 알고 있는 마이그레이션의 사후 조건을 만족시키는 DDL을 실행합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/habit-chain/habit-chain-prepare.jpg&quot; alt=&quot;테이블이 없는 데이터베이스를 연결하면 DB 준비하기 버튼과 스키마 SQL 보기 링크가 나온다&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3단계에서 DB의 스키마 상태를 다시 확인하는 이유는 앞선 시도가 일부만 적용되었거나 완료 응답을 받지 못한 경우에 &lt;code&gt;ALTER TABLE &amp;#8230;&amp;#8203; ADD COLUMN&lt;/code&gt; 등을 무조건 다시 실행하지 않기 위해서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;순번이_있는_마이그레이션_목록&quot;&gt;5.1. 순번이 있는 마이그레이션 목록&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실행할 DDL은 순번이 있는 목록으로 관리합니다. 목록은 뒤에 붙기만 하고, 순번이 의존 순서를 정합니다. 실행 시점에는 적용되지 않은 SQL 구문만 순서대로 골라냅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;worker/src/dolt.ts&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;export const MIGRATIONS: Migration[] = [
  { id: 1, name: &quot;create-habits&quot;, statement: CREATE_HABITS, applied: (s) =&amp;gt; s.habits },
  { id: 2, name: &quot;create-checks&quot;, statement: CREATE_CHECKS, applied: (s) =&amp;gt; s.checks },
  {
    id: 3,
    name: &quot;habits-description&quot;,
    statement: ADD_DESCRIPTION,
    // Covered by create-habits on a fresh DB: the snapshot already has it.
    applied: (s) =&amp;gt; !s.habits || s.description,
  },
];

export function migrations(shape: Shape): string[] {
  return MIGRATIONS.filter((m) =&amp;gt; !m.applied(shape)).map((m) =&amp;gt; m.statement);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 형태는 Flyway나 &lt;a href=&quot;https://github.com/golang-migrate/migrate&quot;&gt;golang-migrate&lt;/a&gt; 같은 마이그레이션 도구에서 가져왔습니다. 다만 도구를 그대로 붙일 수는 없었습니다. 이런 도구는 JDBC 같은 DB 연결을 전제하는데 DoltHub는 REST API만 노출합니다. 그리고 배포자가 자격증명을 갖고 배포 시점에 실행하는 모델인데, 이 앱에서는 사용자의 DB가 배포 이후에 생기고 마이그레이션은 그 사용자의 토큰으로 요청 시점에 돕니다. 그래서 순번 목록이라는 형태만 가져오고 두 가지를 바꿨습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;버전 테이블을 두지 않습니다.&lt;/strong&gt; 마이그레이션 도구는 &lt;code&gt;schema_migrations&lt;/code&gt; 같은 테이블에 적용된 버전을 기록합니다. 이 구성에서는 사용자가 DoltHub 웹 화면에서 스키마를 직접 만질 수 있고, 여러 write 요청을 하나로 묶는 원자적 트랜잭션도 없습니다. DDL은 성공하고 버전 기록은 실패하면 둘이 어긋날 수 있습니다. 그래서 적용 여부는 마이그레이션마다 실제 스키마를 조사한 결과(&lt;code&gt;applied&lt;/code&gt;)로 판정합니다. 실제 스키마가 유일한 기록입니다. 다만 현재 조사는 테이블과 일부 칼럼의 존재처럼 각 마이그레이션이 정한 조건만 확인합니다. 칼럼 타입, 기본값, PK까지 전체 스키마가 정확한지 검증하는 도구는 아닙니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CREATE 구문은 현재 스키마의 스냅숏입니다.&lt;/strong&gt; 도구는 새 DB에도 과거 &lt;code&gt;ALTER&lt;/code&gt;를 순서대로 재생하지만, 여기서는 SQL 구문 하나가 커밋 하나이고 1.5초에서 2초가 걸려서 재생할수록 첫 준비가 느려집니다. 새 테이블은 최신 모양으로 바로 만들고, 그 마이그레이션이 뒤의 &lt;code&gt;ALTER&lt;/code&gt;까지 덮은 것으로 판정합니다. 3번의 &lt;code&gt;applied&lt;/code&gt;가 &quot;habits 테이블이 없으면 적용된 것&quot;으로 읽히는 이유입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자동으로 실행하는 내림(down) 마이그레이션도 만들지 않았습니다. 문제가 생기면 Dolt의 커밋 이력을 보고 수동으로 복구합니다. 스키마와 데이터가 함께 변한 커밋을 되돌리는 일은 별도의 호환성 판단이 필요하므로, 커밋 이력이 down 마이그레이션과 같은 것은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;새_데이터베이스의_branch_not_found_오류&quot;&gt;5.2. 새 데이터베이스의 branch not found 오류&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;새 데이터베이스를 연결한 사용자가 &lt;code&gt;query error: branch not found&lt;/code&gt;를 만났습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원인은 DDL 실패가 아니었습니다. DoltHub에서 방금 만든 데이터베이스는 커밋이 하나도 없어서 &lt;code&gt;main&lt;/code&gt; 브랜치 자체가 없습니다. 목록을 읽는 첫 질의가 거기서 실패한 것이었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl &quot;https://www.dolthub.com/api/v1alpha1/benelog/chain2/main?q=SHOW%20TABLES&quot;
# {&quot;query_execution_status&quot;:&quot;Error&quot;,&quot;query_execution_message&quot;:&quot;query error: branch not found&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 응답을 오류 화면에 그대로 띄우면 사용자가 할 수 있는 일이 없습니다. 지금은 이 메시지를 알아보고 준비 안내로 보냅니다. 다른 오류는 그대로 오류 화면으로 갑니다. 존재하지 않는 데이터베이스 이름까지 준비 안내로 덮으면 진짜 고장을 감추게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;브랜치가 없는 데이터베이스에 write 엔드포인트로 &lt;code&gt;CREATE TABLE&lt;/code&gt;을 보내면 테이블과 함께 브랜치도 생깁니다. 이것도 문서에 없는 동작입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dolthub_저장_구성의_제약&quot;&gt;6. DoltHub 저장 구성의 제약&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 방식을 그대로 따라 하실 분을 위해 걸리는 점을 적습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;공개 데이터베이스는 이름을 아는 누구나 읽을 수 있습니다.&lt;/strong&gt; 토큰 없이 API로 현재 테이블과 커밋 이력을 읽을 수 있으므로, 습관 이름과 날짜뿐 아니라 설명, 생성 시각, note 칼럼과 수정·삭제 전 값도 공개됩니다. 민감한 값을 나중에 지우거나 고쳐도 과거 커밋에서는 사라지지 않습니다. 기록을 감추려면 비공개 DB를 써야 합니다. DoltHub Pro가 비공개 DB를 지원하며 2026년 8월 기준으로 비공개 데이터 100MB까지 무료이고, 초과하면 월 5달러입니다. 앱은 읽기 요청에도 토큰을 전달하므로 비공개 DB도 지원하지만, JSON 내려받기는 토큰을 싣지 않는 일반 링크라 공개 DB에서만 동작합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;쓰기가 1.5초에서 2초 걸립니다.&lt;/strong&gt; write 엔드포인트가 비동기라서 작업을 걸고 완료까지 폴링합니다. 체크 버튼을 누르고 바로 반응하는 화면을 원한다면 맞지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SQL 구문 하나에 커밋 하나가 쌓입니다.&lt;/strong&gt; 영향받은 행이 0건이어도 커밋이 생깁니다. 체크를 자주 하면 커밋 로그가 길어집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;write 엔드포인트는 한 요청에 SQL 구문 하나만 받습니다.&lt;/strong&gt; 세미콜론으로 이어 보내면 &lt;code&gt;Error parsing SQL&lt;/code&gt;로 거부합니다. 습관 삭제는 `checks`와 `habits`를 차례로 지우는 두 요청입니다. 첫 요청 뒤에 두 번째 요청이 실패하면 습관은 남고 체크 기록만 지워진 상태가 되며, 앱은 이를 자동으로 보상하지 않습니다. 이런 부분 실패는 DoltHub의 커밋 이력을 보고 수동으로 복구해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt;의 &lt;code&gt;AFTER&lt;/code&gt;절을 Dolt 파서가 거부합니다.&lt;/strong&gt; 칼럼 위치는 지정하지 않았습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;7. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/habit-chain&quot;&gt;benelog/habit-chain&lt;/a&gt; — 이 글에서 설명한 저장소&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/docs/products/dolthub/api/&quot;&gt;DoltHub API 개요&lt;/a&gt; — 신규 연동에 v2를 권장하는 공식 안내&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/docs/products/dolthub/api/v1alpha1/sql&quot;&gt;DoltHub API: SQL&lt;/a&gt; — 읽기와 쓰기 엔드포인트 문서&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/blog/2026-07-09-dolthub-api-v2/&quot;&gt;DoltHub API v2 발표&lt;/a&gt; — v2의 계약과 &lt;code&gt;v1alpha1&lt;/code&gt; 지원 지속 안내&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/blog/2023-08-11-fine-grained-permissions-and-enhanced-dolthub-api/&quot;&gt;DoltHub API 토큰 권한&lt;/a&gt; — DB, scope, 읽기·쓰기 권한을 제한하는 토큰&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/docs/sql-reference/version-control/dolt-system-tables&quot;&gt;Dolt: 버전 관리 시스템 테이블&lt;/a&gt; — &lt;code&gt;dolt_log&lt;/code&gt;와 &lt;code&gt;dolt_history_&amp;lt;테이블 이름&amp;gt;&lt;/code&gt;의 스키마&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/docs/cli-reference/cli/&quot;&gt;Dolt CLI: clone&lt;/a&gt; — DB와 버전 이력을 로컬로 가져오는 명령&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dolthub.com/blog/2026-04-24-announcing-dolthub-pro-for-5-dollars-per-month/&quot;&gt;DoltHub Pro 요금 발표&lt;/a&gt; — 비공개 데이터베이스의 무료 한도와 요금&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://developers.cloudflare.com/pages/functions/advanced-mode/&quot;&gt;Cloudflare Pages: Advanced mode&lt;/a&gt; — &lt;code&gt;_worker.js&lt;/code&gt;하나로 모든 요청을 받는 구성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://htmx.org/attributes/hx-headers/&quot;&gt;htmx: hx-headers&lt;/a&gt; — 상속되는 속성과 &lt;code&gt;js:&lt;/code&gt;접두사&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/golang-migrate/migrate&quot;&gt;golang-migrate&lt;/a&gt; — 순번 마이그레이션 목록의 원형이 된 도구&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code, Codex와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>설정 파일이 없는 FTP 서버, one-ftpserver</title>
      <link>https://blog.benelog.net//one-ftpserver.html</link>
      <pubDate>Sun, 16 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">one-ftpserver.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#배경&quot;&gt;배경&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#설치와_실행&quot;&gt;설치와 실행&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#명령행_옵션&quot;&gt;명령행 옵션&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ftps&quot;&gt;FTPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#접속_기록_로그&quot;&gt;접속 기록 로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정해진_시간_뒤_자동_종료&quot;&gt;정해진 시간 뒤 자동 종료&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#nat와_컨테이너_뒤에서_실행&quot;&gt;NAT와 컨테이너 뒤에서 실행&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#스크립트에서_쓰는_json_출력&quot;&gt;스크립트에서 쓰는 JSON 출력&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#v0_1_java_버전과_달라진_점&quot;&gt;v0.1 Java 버전과 달라진 점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#구현에서_눈여겨본_부분&quot;&gt;구현에서 눈여겨본 부분&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#한_명만_인증하는_driver&quot;&gt;한 명만 인증하는 driver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#홈_디렉터리_밖을_감추는_파일_시스템&quot;&gt;홈 디렉터리 밖을 감추는 파일 시스템&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#실행할_때마다_새로_만드는_인증서&quot;&gt;실행할 때마다 새로 만드는 인증서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#실제_소켓으로_도는_통합_테스트&quot;&gt;실제 소켓으로 도는 통합 테스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버 작업을 하다 보면 PC에 있는 파일을 서버에 올리거나, 서버에 있는 로그 파일을 PC로 받고 싶을 때가 있습니다.
파일 하나를 옮기는 정도라면 다운로드는 &lt;a href=&quot;2875999.html&quot;&gt;Winstone&lt;/a&gt;으로, 업로드는 간단한 웹 애플리케이션인 &lt;a href=&quot;https://github.com/benelog/uploader&quot;&gt;uploader&lt;/a&gt;로 했습니다.
Python이 설치된 서버라면 &lt;code&gt;python3 -m http.server&lt;/code&gt;로 웹 서버를 띄우는 방법도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 개의 파일을 주고받을 때는 FTP가 편합니다.
그런데 대부분의 FTP 서버는 설정 파일을 만들고 사용자 목록을 따로 적어야 실행됩니다.
패키지를 내려받고, 압축을 풀고, 설정 파일을 편집하는 과정을 여러 번 반복하다 보니 실행할 때 옵션만 주면 바로 뜨는 서버가 필요했습니다.
그래서 2012년에 &lt;a href=&quot;https://mina.apache.org/ftpserver-project/&quot;&gt;Apache FtpServer&lt;/a&gt;를 감싼 실행 가능한 jar 파일을 만들어서 &lt;a href=&quot;https://github.com/benelog/one-ftpserver&quot;&gt;one-ftpserver&lt;/a&gt;라는 이름으로 공개했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 8월에는 같은 프로그램을 Go로 다시 작성해서 2.0.0을 냈습니다.
2.0.0부터는 JVM 없이 실행 파일 하나로 배포하고, jar 안에 넣어 두었던 FTPS용 key store도 없앴습니다.
이 글은 2026년 8월에 나온 2.2.0을 기준으로 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;설정 파일을 읽지도, 쓰지도 않습니다. 모든 설정은 명령행 옵션이고, &lt;code&gt;--help&lt;/code&gt;가 전체 목록입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;바이너리 파일 하나로 실행됩니다. Linux와 Windows용으로 amd64, arm64 바이너리가 제공됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FTPS에 key store가 필요 없습니다. &lt;code&gt;--ssl&lt;/code&gt;만 주면 인증서를 실행할 때 메모리에서 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;사용자 데이터베이스가 없습니다. 사용자는 &lt;code&gt;--id&lt;/code&gt;, &lt;code&gt;--password&lt;/code&gt;로 지정하는 한 명뿐입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;클라이언트가 올린 파일과 접속 기록 로그 외에는 아무것도 남기지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;설치와_실행&quot;&gt;설치와 실행&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/one-ftpserver/releases/latest&quot;&gt;릴리스 페이지&lt;/a&gt;에서 플랫폼에 맞는 바이너리를 받습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl -LO https://github.com/benelog/one-ftpserver/releases/latest/download/one-ftpserver-linux-amd64
mv one-ftpserver-linux-amd64 one-ftpserver
chmod +x one-ftpserver&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포하는 파일은 다음의 4개입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;one-ftpserver-linux-amd64&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;one-ftpserver-linux-arm64&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;one-ftpserver-windows-amd64.exe&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;one-ftpserver-windows-arm64.exe&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;macOS에서는 &lt;code&gt;go build&lt;/code&gt;로 직접 빌드하셔서 사용하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아무 옵션 없이 실행하면 현재 디렉터리를 2121번 포트로 공유하고 익명 사용자(anonymous) 로그인을 받습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;포트, 아이디, 비밀번호, 공유할 디렉터리는 명령행에서 바로 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --port=10021 --id=benelog --password=1234 --home=/srv/files&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버가 실행되면 적용된 설정과 함께, 그 설정에 맞는 클라이언트 명령을 계정 정보까지 채워서 출력합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;FTP server started : ftp://192.168.0.10:10021

# Settings
- ssl : false
- port : 10021
- passivePorts : [none]
- id : benelog
- password : 1234
- home : /srv/files
- timeout : [none]
- log : /home/benelog/one-ftpserver-2026-08-16.log

# Client commands
- upload : curl -T [filename] ftp://192.168.0.10:10021/ -u benelog:1234
- download : curl -O ftp://192.168.0.10:10021/[filename] -u benelog:1234
- download : wget --user=benelog --password=1234 ftp://192.168.0.10:10021/[filename]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 명령을 그대로 복사해서 다른 PC에서 실행하면 파일을 주고받을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;명령행_옵션&quot;&gt;명령행 옵션&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;옵션&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;기본값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--port&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;2121&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제어 포트. &lt;code&gt;0&lt;/code&gt;을 주면 OS가 고른 포트를 잡고, 그 번호를 출력합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--home&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;.&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;공유할 디렉터리. 클라이언트는 이 디렉터리 위로 올라갈 수 없습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--id&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;anonymous&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;로그인할 수 있는 유일한 사용자.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--password&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;그 사용자의 비밀번호. &lt;code&gt;--id&lt;/code&gt;만 주면 실행할 때 생성해서 출력합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--passivePorts&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;passive 전송에 쓸 포트 범위. &lt;code&gt;10125-10199&lt;/code&gt; 형식으로 지정하고, 비워 두면 OS가 고릅니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--ssl&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;false&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실행할 때 만든 인증서로 TLS 위에서 서비스합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--cert&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;직접 준비한 인증서 PEM 파일. 지정하면 &lt;code&gt;--ssl&lt;/code&gt;이 켜집니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--key&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;그 인증서의 개인 키 PEM 파일.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--timeout&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;0&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;30m&lt;/code&gt;처럼 지정한 시간이 지나면 서버가 스스로 종료합니다. &lt;code&gt;0&lt;/code&gt;이면 계속 떠 있습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--publicHost&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;클라이언트에 알려 줄 주소. 비워 두면 서버가 찾아냅니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--json&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;false&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;설정 요약을 텍스트 대신 JSON으로 출력합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--log&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;one-ftpserver.log&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;접속 기록을 남길 파일. 날짜별로 한 개씩 만들고, &lt;code&gt;off&lt;/code&gt;를 주면 남기지 않습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--console&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;false&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;접속 기록을 콘솔에도 출력합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ftps&quot;&gt;FTPS&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --ssl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--ssl&lt;/code&gt;을 주면 implicit FTPS로 동작합니다. &lt;code&gt;ftps://&lt;/code&gt; URL로 사용하는 방식입니다.
인증서는 서버가 시작할 때 메모리에서 만듭니다. 유효기간은 1년이고 &lt;code&gt;localhost&lt;/code&gt;와 그 장비의 IP 주소를 담으며, 서버를 다시 띄우면 새로 만듭니다.
아무도 서명하지 않은 인증서라서 클라이언트가 검증할 방법이 없고, 그래서 서버가 출력하는 &lt;code&gt;curl&lt;/code&gt; 명령에는 &lt;code&gt;-k&lt;/code&gt;가 붙습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl -O ftps://192.168.0.10:2121/report.pdf -k&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 인증서는 전송 구간을 암호화할 뿐, 서버가 누구인지는 증명하지 못합니다.
서로 아는 두 사람이 신뢰할 수 없는 네트워크를 거쳐 파일을 주고받는 상황에는 충분하지만, 모르는 사람이 접속하는 서버라면 직접 발급받은 인증서를 PEM 파일 한 쌍으로 넘기는 편이 낫습니다.
한 쌍을 넘기면 &lt;code&gt;--ssl&lt;/code&gt;은 저절로 켜지고, 출력되는 명령에서 &lt;code&gt;-k&lt;/code&gt;가 빠집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --cert=fullchain.pem --key=privkey.pem&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인증서를 무료로 발급받는 방법으로는 &lt;a href=&quot;https://letsencrypt.org/&quot;&gt;Let&amp;#8217;s Encrypt&lt;/a&gt;가 편합니다.
도메인이 가리키는 장비에서 80번 포트를 열어 두고 발급받습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo certbot certonly --standalone -d ftp.example.com

one-ftpserver --cert=/etc/letsencrypt/live/ftp.example.com/fullchain.pem \
              --key=/etc/letsencrypt/live/ftp.example.com/privkey.pem&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;직접 발급받은 인증서를 쓸 때 걸리기 쉬운 부분이 세 가지 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;클라이언트는 접속에 쓴 이름을 인증서와 대조하므로, IP 주소가 아니라 발급받은 도메인으로 접속하게 해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/etc/letsencrypt&lt;/code&gt; 아래의 파일은 root만 읽을 수 있습니다. 서버를 root로 실행하거나, 읽을 수 있는 위치로 복사해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;인증서 파일은 시작할 때 한 번만 읽습니다. Let&amp;#8217;s Encrypt 인증서는 90일마다 갱신되므로 갱신 후에는 서버를 다시 띄워야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;접속_기록_로그&quot;&gt;접속 기록 로그&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로그인, 파일 전송, 목록 조회, 삭제, 이름 변경을 모두 기록합니다.
기본으로는 실행한 위치에 날짜가 붙은 파일을 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;one-ftpserver-2026-08-16.log&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;날짜가 바뀌면 새 파일을 엽니다. 이름을 바꾸거나 지우는 처리는 하지 않으므로, 오래 띄워 둔 서버는 하루에 한 개씩 파일을 남깁니다.
&lt;code&gt;--log&lt;/code&gt;로 위치를 옮기고, &lt;code&gt;off&lt;/code&gt;로 로그를 끕니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --log=/var/log/ftp.log
one-ftpserver --log=off&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--console&lt;/code&gt;은 같은 내용을 터미널에도 출력합니다. 전송이 되는지 눈으로 확인하려고 띄운 서버에 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --console&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;time=2026-08-16T10:27:25 level=INFO msg=login client=1 from=192.168.0.20:51000 id=benelog
time=2026-08-16T10:27:25 level=INFO msg=download client=1 from=192.168.0.20:51000 id=benelog path=/report.pdf
time=2026-08-16T10:27:26 level=INFO msg=&quot;download done&quot; client=1 from=192.168.0.20:51000 id=benelog path=/report.pdf bytes=182004&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 옵션은 독립적입니다. &lt;code&gt;--console&lt;/code&gt;만 주면 파일과 터미널에 함께 쓰고, &lt;code&gt;--console --log=off&lt;/code&gt;를 주면 터미널에만 씁니다.
접속 기록은 표준 에러로 나가므로 설정 요약과 &lt;code&gt;--json&lt;/code&gt; 출력은 표준 출력에 그대로 남습니다.
비밀번호는 로그에 남기지 않습니다. 로그인에 실패한 요청이 보낸 값도 마찬가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정해진_시간_뒤_자동_종료&quot;&gt;정해진 시간 뒤 자동 종료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파일 하나를 옮기려고 띄운 서버가 그 뒤로도 계속 떠 있을 이유는 없습니다.
&lt;code&gt;--timeout&lt;/code&gt;에 지정한 시간이 지나면 서버가 스스로 종료합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --timeout=30m&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;nat와_컨테이너_뒤에서_실행&quot;&gt;NAT와 컨테이너 뒤에서 실행&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;passive 전송은 클라이언트가 다시 접속할 주소를 서버가 알려 주는 방식인데, 서버가 보는 주소와 클라이언트가 접근할 수 있는 주소가 다를 때가 있습니다.
&lt;code&gt;--publicHost&lt;/code&gt;로 알려 줄 주소를, &lt;code&gt;--passivePorts&lt;/code&gt;로 방화벽에서 열거나 컨테이너에서 노출할 만큼 좁은 포트 범위를 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --publicHost=203.0.113.10 --passivePorts=10125-10199&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;docker run -p 2121:2121 -p 10125-10199:10125-10199 ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스크립트에서_쓰는_json_출력&quot;&gt;스크립트에서 쓰는 JSON 출력&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--json&lt;/code&gt;은 텍스트 요약 대신 같은 내용을 담은 JSON 객체 하나를 출력합니다.
주소와 계정 정보를 텍스트에서 파싱하지 않고 꺼낼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;one-ftpserver --id=benelog --json&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;address&quot;: &quot;ftp://192.168.0.10:2121&quot;,
  &quot;protocol&quot;: &quot;ftp&quot;,
  &quot;host&quot;: &quot;192.168.0.10&quot;,
  &quot;port&quot;: 2121,
  &quot;id&quot;: &quot;benelog&quot;,
  &quot;password&quot;: &quot;UEPH6KBIXDUIVEKC2Q23U3L4QI&quot;,
  &quot;anonymous&quot;: false,
  &quot;home&quot;: &quot;/srv/files&quot;,
  &quot;ssl&quot;: false,
  &quot;log&quot;: &quot;/home/benelog/one-ftpserver-2026-08-16.log&quot;,
  &quot;upload&quot;: &quot;curl -T [filename] ftp://192.168.0.10:2121/ -u benelog:UEPH6KBIXDUIVEKC2Q23U3L4QI&quot;,
  &quot;download&quot;: &quot;curl -O ftp://192.168.0.10:2121/[filename] -u benelog:UEPH6KBIXDUIVEKC2Q23U3L4QI&quot;,
  &quot;get&quot;: &quot;wget --user=benelog --password=UEPH6KBIXDUIVEKC2Q23U3L4QI ftp://192.168.0.10:2121/[filename]&quot;,
  &quot;warnings&quot;: [
    &quot;no --password was given, so this one was generated for this run&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--port=0&lt;/code&gt;과 같이 쓰면 포트를 정하지 않고 여러 대를 동시에 띄울 수 있습니다. 각 서버가 배정받은 포트를 출력합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;v0_1_java_버전과_달라진_점&quot;&gt;v0.1 Java 버전과 달라진 점&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2.0.0은 Java로 만들었던 0.1을 Go로 다시 쓴 버전입니다. 실행 방법과 옵션 형식이 함께 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;배포: &lt;code&gt;java -jar one-ftpserver.jar&lt;/code&gt; 대신 플랫폼별 바이너리를 실행합니다. Maven 빌드는 없어졌습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;옵션: &lt;code&gt;port=10021&lt;/code&gt;처럼 쓰던 &lt;code&gt;key=value&lt;/code&gt; 형식이 &lt;code&gt;--port=10021&lt;/code&gt; 형식으로 바뀌었습니다. &lt;code&gt;--ssl&lt;/code&gt;은 값을 받지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FTPS: 예전에는 jar 안에 넣어 둔 &lt;code&gt;ftpkeystore.jks&lt;/code&gt;를 실행할 때마다 작업 디렉터리로 복사했고, 그 비밀번호가 소스 코드에 적혀 있었습니다. 지금은 실행할 때 메모리에서 인증서를 만들고 디스크에는 아무것도 쓰지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;비밀번호: &lt;code&gt;--id&lt;/code&gt;만 주면 비밀번호가 아이디와 같아지던 동작을 고쳤습니다. 이제는 실행할 때마다 새로 만들어 출력합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;홈 디렉터리: 클라이언트가 &lt;code&gt;--home&lt;/code&gt; 위쪽 경로에 접근할 수 있던 문제를 고쳤습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;익명 로그인: 클라이언트가 실제로 보내는 &lt;code&gt;anonymous&lt;/code&gt;와 &lt;code&gt;ftp&lt;/code&gt;를 아이디로 받고, 비밀번호는 검사하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;구현에서_눈여겨본_부분&quot;&gt;구현에서 눈여겨본 부분&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FTP 프로토콜은 &lt;a href=&quot;https://github.com/fclairamb/ftpserverlib&quot;&gt;ftpserverlib&lt;/a&gt;가 처리하고, 파일 시스템은 &lt;a href=&quot;https://github.com/spf13/afero&quot;&gt;afero&lt;/a&gt;로 감쌉니다.
One FTP Server가 더하는 부분은 사용자 한 명을 인증하는 driver, 실행할 때 만드는 인증서, 그리고 설정 요약 출력입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;한_명만_인증하는_driver&quot;&gt;한 명만 인증하는 driver&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 버전에서는 Apache FtpServer의 &lt;code&gt;UserManager&lt;/code&gt; 인터페이스를 구현해서 아이디와 비밀번호 한 쌍만 검사하는 &lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/0.1/src/main/java/net/benelog/oneftpserver/user/SingleUserManager.java&quot;&gt;SingleUserManager&lt;/a&gt;를 만들었습니다.
Go 버전에서 그 역할을 하는 코드는 &lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/main/internal/oneftpserver/driver.go&quot;&gt;internal/oneftpserver/driver.go&lt;/a&gt;의 &lt;code&gt;authenticate&lt;/code&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;driver.go&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;func (d *driver) authenticate(user, pass string) bool {
	if d.config.Anonymous() {
		// &quot;ftp&quot; is the other name clients traditionally send for an anonymous
		// login, and the password is an e-mail address nobody checks.
		return user == AnonymousID || user == &quot;ftp&quot;
	}

	// Constant time by contract. Go&apos;s == makes no such promise: it happens to
	// compare 8 bytes at a time, so a short password gives an attacker nothing
	// to time today, but that is an implementation detail and not a guarantee.
	sameUser := subtle.ConstantTimeCompare([]byte(user), []byte(d.config.ID)) == 1
	samePassword := subtle.ConstantTimeCompare([]byte(pass), []byte(d.config.Password)) == 1

	return sameUser &amp;amp;&amp;amp; samePassword
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;==&lt;/code&gt; 대신 &lt;code&gt;crypto/subtle&lt;/code&gt;의 &lt;code&gt;ConstantTimeCompare&lt;/code&gt;를 쓴 이유는 상수 시간 비교를 보장받기 위해서입니다.
비교가 다른 바이트를 만나는 순간 멈추면 걸린 시간에 앞에서 몇 글자가 맞았는지가 드러난다는 것이 상수 시간 비교를 권하는 흔한 이유입니다.
다만 Go의 &lt;code&gt;==&lt;/code&gt;가 실제로 그렇게 새지는 않습니다.
16바이트 문자열을 Go 1.26.7에서 재 보면 불일치 위치가 0~7일 때 3.11ns, 8~15일 때 3.82ns로 두 값뿐이고 완전히 일치할 때는 그 사이인 3.48ns입니다.
&lt;code&gt;runtime.memequal&lt;/code&gt;이 바이트 단위가 아니라 8바이트씩 한 번에 비교하기 때문에 글자마다 시간이 늘어나는 구간이 없고, 오래 걸렸다고 더 많이 맞은 것도 아닙니다.
원격에서 응답 시간을 측정할 수 있는 한계가 LAN에서 100ns 수준이라는 보고를 감안하면 이 정도 차이는 신호가 되지 못합니다.
그래도 &lt;code&gt;ConstantTimeCompare&lt;/code&gt;를 쓰는 이유는 &lt;code&gt;==&lt;/code&gt;가 상수 시간을 약속하지 않는다는 데 있습니다.
길이나 아키텍처, 런타임 구현이 바뀌면 위와 같이 다시 재야 하는데, 보장이 문서에 적힌 API를 쓰면 그럴 일이 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로그인을 거절할 때는 아이디가 틀렸는지 비밀번호가 틀렸는지 구분하지 않고 같은 오류를 돌려줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;홈_디렉터리_밖을_감추는_파일_시스템&quot;&gt;홈 디렉터리 밖을 감추는 파일 시스템&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인증에 성공한 클라이언트에는 홈 디렉터리를 최상위로 하는 파일 시스템을 넘깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;driver.go&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;fs := afero.NewBasePathFs(afero.NewOsFs(), d.config.Home)
hiding := &amp;amp;pathHidingFs{Fs: fs, home: d.config.Home}

return &amp;amp;loggingFs{Fs: hiding, logger: logger}, nil&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;afero.NewBasePathFs&lt;/code&gt;가 홈 디렉터리 위로 올라가지 못하게 막는 부분입니다.
그런데 이 파일 시스템이 내는 오류 메시지에는 디스크의 전체 경로가 담기고, FTP 라이브러리는 그 메시지를 클라이언트에 그대로 전달합니다.
그래서 &lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/main/internal/oneftpserver/fs.go&quot;&gt;fs.go&lt;/a&gt;의 &lt;code&gt;pathHidingFs&lt;/code&gt;로 한 번 더 감싸서, 오류에 담긴 경로를 클라이언트가 사용한 경로로 바꿉니다.
그 위에 로그를 남기는 &lt;code&gt;loggingFs&lt;/code&gt;를 씌우기 때문에, 기록할 동작을 추가할 자리는 라이브러리가 아니라 이 래퍼입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;실행할_때마다_새로_만드는_인증서&quot;&gt;실행할 때마다 새로 만드는 인증서&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/main/internal/oneftpserver/tls.go&quot;&gt;tls.go&lt;/a&gt;의 &lt;code&gt;selfSignedTLSConfig&lt;/code&gt;는 P-256 키와 자체 서명 인증서를 만들어 &lt;code&gt;tls.Config&lt;/code&gt;에 바로 담습니다.
인증서에는 &lt;code&gt;localhost&lt;/code&gt;와 그 장비의 IP 주소를 넣습니다. 클라이언트가 어느 인터페이스로 들어오든 접속에 쓴 주소가 인증서와 맞도록 하기 위해서입니다.
파일로 쓰지 않기 때문에 &lt;code&gt;--ssl&lt;/code&gt;이 옵션 하나로 끝나고, 서버를 끄고 나면 키가 남지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;실제_소켓으로_도는_통합_테스트&quot;&gt;실제 소켓으로 도는 통합 테스트&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;옵션 조합이 의도대로 동작하는지 확인하려면 반복 테스트가 필요합니다.
Java 버전에서는 Apache Commons Net의 &lt;code&gt;FTPClient&lt;/code&gt;로 통합 테스트를 만들었는데, Go 버전에서는 테스트에 필요한 만큼의 RFC 959 명령만 직접 구현한 클라이언트를 씁니다.
&lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/main/internal/oneftpserver/server_test.go&quot;&gt;server_test.go&lt;/a&gt;에서 서버를 실제로 띄우고 소켓으로 접속합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;server_test.go&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;func TestAFileCanBeUploaded(t *testing.T) {
	home := t.TempDir()
	summary := startServer(t, &amp;amp;Config{ID: &quot;benelog&quot;, Password: &quot;1234&quot;, Home: home})

	client := dial(t, summary.Port, false)
	client.login(&quot;benelog&quot;, &quot;1234&quot;)
	client.store(&quot;uploaded.txt&quot;, &quot;my works&quot;)

	stored, err := os.ReadFile(filepath.Join(home, &quot;uploaded.txt&quot;))
	if err != nil {
		t.Fatalf(&quot;the uploaded file is not on disk: %v&quot;, err)
	}
	if string(stored) != &quot;my works&quot; {
		t.Errorf(&quot;stored %q, want %q&quot;, stored, &quot;my works&quot;)
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트는 모두 &lt;code&gt;--port=0&lt;/code&gt;으로 서버를 띄웁니다. 포트를 OS가 고르므로 여러 테스트를 동시에 돌려도 포트가 겹치지 않습니다.
FTPS 테스트도 같은 클라이언트를 &lt;code&gt;tls.Dial&lt;/code&gt;로 연결해서 씁니다. implicit FTPS라서 &lt;code&gt;AUTH TLS&lt;/code&gt;를 주고받는 절차 없이 첫 바이트부터 암호화됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;품질 도구는 &lt;a href=&quot;go-quality-tools.html&quot;&gt;Go 코드에 품질 도구 적용하기&lt;/a&gt;에 정리한 구성을 그대로 씁니다.
&lt;code&gt;make check&lt;/code&gt;가 goimports, golangci-lint, 테스트를 차례로 실행하고, &lt;code&gt;make ci&lt;/code&gt;는 GitHub Actions에서 push와 pull request마다 도는 명령입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/one-ftpserver&quot;&gt;one-ftpserver 저장소&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/main/CHANGELOG.md&quot;&gt;CHANGELOG.md&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/one-ftpserver/blob/main/DEVELOPMENT.md&quot;&gt;DEVELOPMENT.md&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cs.rice.edu/~dwallach/pub/crosby-timing2009.pdf&quot;&gt;Opportunities and Limits of Remote Timing Attacks&lt;/a&gt; (원격 타이밍 측정의 한계를 다룬 논문)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go 라이브러리&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/fclairamb/ftpserverlib&quot;&gt;fclairamb/ftpserverlib&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spf13/afero&quot;&gt;spf13/afero&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://mina.apache.org/ftpserver-project/&quot;&gt;Apache FtpServer&lt;/a&gt; (0.1 버전이 감싼 Java FTP 서버)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;go-quality-tools.html&quot;&gt;Go 코드에 품질 도구 적용하기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;2875999.html&quot;&gt;Winstone, 경량 Servlet container&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;2864739.html&quot;&gt;간단하게 서버에 파일을 올리기 + MockMultipartFile을 이용한 테스트&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;주요 변경이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2026.09.05&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Go로 다시 만든 2.x 기준으로 새로 씀&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ConstantTimeCompare&lt;/code&gt;를 쓴 이유를 실측 결과로 다시 씀&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2012.05.17&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java 버전 소개 글 최초 작성&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>IEEE 754 부동소수점 오차와 Java의 대안</title>
      <link>https://blog.benelog.net//floating-point-java.html</link>
      <pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">floating-point-java.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#코드로_확인하는_부동소수점_오차&quot;&gt;1. 코드로 확인하는 부동소수점 오차&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#doubleieee_754_binary64의_비트_구조와_0_1의_저장_과정&quot;&gt;2. double(IEEE 754 binary64)의 비트 구조와 0.1의 저장 과정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2진수_표현_오차로_생긴_장애_사례&quot;&gt;3. 2진수 표현 오차로 생긴 장애 사례&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#2011년_neis_성적_처리_오류&quot;&gt;3.1. 2011년 NEIS 성적 처리 오류&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#1991년_걸프전_패트리어트_미사일_요격_실패&quot;&gt;3.2. 1991년 걸프전 패트리어트 미사일 요격 실패&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#자바에서_소수점_연산_오차를_피하는_대안&quot;&gt;4. 자바에서 소수점 연산 오차를 피하는 대안&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#bigdecimal로_계산하기&quot;&gt;4.1. BigDecimal로 계산하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정수로_계산하기&quot;&gt;4.2. 정수로 계산하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#bigdecimal과_정수_계산의_선택_기준&quot;&gt;5. BigDecimal과 정수 계산의 선택 기준&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#금액과_단위를_다루는_도메인_특화_라이브러리&quot;&gt;6. 금액과 단위를 다루는 도메인 특화 라이브러리&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#java_money_jsr_354&quot;&gt;6.1. Java Money (JSR 354)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#그_밖의_도메인_특화_라이브러리&quot;&gt;6.2. 그 밖의 도메인 특화 라이브러리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;7. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자바의 &lt;code&gt;double&lt;/code&gt;이나 &lt;code&gt;float&lt;/code&gt; 자료형은 IEEE 754 부동소수점 표준에 따라 값을 2진수로 저장합니다.
이 방식으로는 십진수 0.1을 정확하게 표현할 수 없습니다.
0.1은 2진수로는 무한히 반복되는 소수가 되기 때문에, 유한한 비트 안에 담으려면 가장 가까운 값으로 반올림해야 합니다.
이 글에서는 그런 오차가 왜 생기는지, 어떤 경우에 위험한지를 실제 장애 사례와 함께 살펴보고, 자바에서 쓸 수 있는 대안을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;코드로_확인하는_부동소수점_오차&quot;&gt;1. 코드로 확인하는 부동소수점 오차&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;부동소수점 오차는 jshell에서 간단히 확인해 볼 수 있습니다.
jshell은 JDK 9부터 포함된 대화형 실행 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;소수 더하기 연산&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; 0.1 + 0.2
$1 ==&amp;gt; 0.30000000000000004

jshell&amp;gt; 0.1 + 0.2 == 0.3
$2 ==&amp;gt; false

jshell&amp;gt; 1.03 - 0.42
$3 ==&amp;gt; 0.6100000000000001&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;0.1을 열 번 더해도 1.0이 되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;0.1 열 번 더하기&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; double sum = 0;
sum ==&amp;gt; 0.0

jshell&amp;gt; for (int i = 0; i &amp;lt; 10; i++) { sum += 0.1; }

jshell&amp;gt; sum
sum ==&amp;gt; 0.9999999999999999&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;double&lt;/code&gt; 리터럴 0.1에 실제로 저장된 값은 &lt;code&gt;BigDecimal&lt;/code&gt; 생성자로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;double 리터럴 0.1의 실제 값&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; new BigDecimal(0.1)
$1 ==&amp;gt; 0.1000000000000000055511151231257827021181583404541015625&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jshell 명령행에서 0.1로 입력했지만 저장된 값은 0.1보다 약 5.55 × 10&lt;sup&gt;-18&lt;/sup&gt;만큼 큰 수입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 오차는 자바만의 문제가 아닙니다.
JavaScript의 &lt;code&gt;number&lt;/code&gt; 타입도 IEEE 754 binary64 형식을 쓰기 때문에, Chrome 개발자 도구 콘솔에서 같은 식을 입력하면 결과가 똑같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/floating-point-java/chrome-console-float.png&quot; alt=&quot;Chrome 개발자 도구 콘솔에서 확인한 부동소수점 오차&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;왜 이런 값이 저장되는지는 다음 절에서 &lt;code&gt;double&lt;/code&gt;의 비트 구조를 따라가며 살펴봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;doubleieee_754_binary64의_비트_구조와_0_1의_저장_과정&quot;&gt;2. double(IEEE 754 binary64)의 비트 구조와 0.1의 저장 과정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IEEE 754는 여러 부동소수점 형식을 정의하는 표준입니다.
그중 자바가 쓰는 두 형식은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;형식&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;1985년 명칭&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;크기&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;자바 자료형&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;binary32&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;single&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;32비트 (부호 1 / 지수 8 / 가수 23)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;float&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;binary64&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;double&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;64비트 (부호 1 / 지수 11 / 가수 52)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;double&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1985년에 나온 첫 표준인 IEEE 754-1985에서는 이 두 형식을 single, double이라고 불렀습니다.
2008년 개정판에서 이름이 binary32, binary64로 바뀌었고, 이 이름이 현행 표준인 IEEE 754-2019까지 이어집니다.
표준에는 이 외에도 binary16, binary128과 10진 형식인 decimal32, decimal64, decimal128이 들어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IEEE 754의 2진 형식은 실수를 2진수 과학적 표기법으로 바꾼 다음 비트로 저장합니다.
십진수 과학적 표기법에서는 소수점을 옮겨 정수부를 1~9 사이 한 자리로 만들고, 옮긴 자릿수를 10의 지수로 나타냅니다.
예를 들어 0.001은 1.0 × 10&lt;sup&gt;-3&lt;/sup&gt;으로 씁니다.
2진수에서도 같은 원리로 2를 곱하거나 나눠서 정수부가 한 자리가 되도록, 즉 값이 1 이상 2 미만이 되도록 만듭니다.
2진수에서 0이 아닌 한 자리 숫자는 1뿐이므로, 이렇게 정규화한 결과는 언제나 ±1.가수 × 2&lt;sup&gt;지수&lt;/sup&gt; 형태가 됩니다.
예를 들어 2진수 0.011(십진수 0.375)은 1.1 × 2&lt;sup&gt;-2&lt;/sup&gt;으로 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;double&lt;/code&gt; (binary64)은 이렇게 정규화한 값을 64비트에 담으면서 다음 규칙을 따릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;부호(s)는 맨 앞 1비트에 저장합니다. 0이면 양수, 1이면 음수입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;지수(e)는 실제 지수에 1023을 더한 값을 11비트에 저장합니다. 이렇게 하면 -1022~1023의 지수를 1~2046의 부호 없는 정수로 저장할 수 있고, 값을 읽을 때는 거꾸로 1023을 뺍니다. 11비트로 나타낼 수 있는 0~2047 중 양 끝의 두 값은 용도가 따로 있어서, 0은 0과 0에 아주 가까운 수를, 2047은 무한대와 NaN을 나타냅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;가수(f)는 1.가수에서 소수점 아래 부분만 52비트에 저장합니다. 정규화된 수는 정수부가 항상 1이므로 그 1은 생략합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;소수점 아래가 52비트를 넘으면 가장 가까운 값으로 반올림합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 규칙대로 0.1을 저장한 결과가 아래 그림입니다.
64비트가 부호 1비트, 지수 11비트, 가수 52비트로 나뉘어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/floating-point-java/ieee754-double.png&quot; alt=&quot;IEEE 754 double의 비트 구조와 0.1 저장 예&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그림 아래쪽의 변환 과정을 따라가 보겠습니다.
0.1에 2를 거듭 곱해 보면 0.2, 0.4, 0.8을 거쳐 네 번째에 1.6이 되어 처음으로 1 이상 2 미만 구간에 들어옵니다.
2를 네 번 곱해서 1.6이 되었으니 거꾸로 0.1 = 1.6 ÷ 2&lt;sup&gt;4&lt;/sup&gt; = 1.6 × 2&lt;sup&gt;-4&lt;/sup&gt;입니다.
따라서 부호 비트는 0, 지수부는 -4 + 1023 = 1019가 됩니다.
문제는 가수입니다.
0.6을 2진수로 바꾸면 0.1001 1001 1001…처럼 1001이 끝없이 반복됩니다.
52비트 안에 담으려면 반올림할 수밖에 없습니다.
52비트 뒤에 이어지는 비트는 1001…이어서, 버려지는 부분이 마지막 비트 크기의 절반보다 큽니다.
그래서 올림이 일어나 가수의 마지막 네 비트 1001이 1010이 됩니다.
그 결과 0.1이 아니라 0.1보다 아주 조금 큰 수가 저장됩니다.
앞에서 &lt;code&gt;new BigDecimal(0.1)&lt;/code&gt;로 확인한 0.1000000000000000055511151231257827021181583404541015625가 바로 이 반올림된 값입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저장된 비트는 jshell에서 16진수로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;0.1을 저장한 64비트&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; Long.toHexString(Double.doubleToLongBits(0.1))
$1 ==&amp;gt; &quot;3fb999999999999a&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;16진수 한 자리는 4비트입니다.
맨 앞의 세 자리 3fb는 부호 1비트와 지수 11비트를 합친 12비트로, 부호 비트는 0이고 지수부는 0x3fb, 십진수로 1019입니다.
나머지 열세 자리가 가수 52비트입니다.
9(1001)가 열두 번 반복되다가 마지막 자리만 a(1010)로 올림되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모든 소수에서 오차가 생기는 것은 아닙니다.
0.5(1/2), 0.25(1/4), 0.75(3/4)처럼 분모가 2의 거듭제곱인 소수는 유한한 2진 소수가 되므로, 유효 비트가 53개를 넘지 않으면 정확하게 저장됩니다.
반면 0.1 = 1/10처럼 기약분수로 나타냈을 때 분모에 5가 들어 있는 십진 소수는 2진수로는 무한히 반복되는 소수가 되어 반올림 오차를 피할 수 없습니다.
표준을 더 자세히 알고 싶다면 &lt;a href=&quot;https://ko.wikipedia.org/wiki/IEEE_754&quot;&gt;IEEE 754 위키백과 문서&lt;/a&gt;를 참고할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;2진수_표현_오차로_생긴_장애_사례&quot;&gt;3. 2진수 표현 오차로 생긴 장애 사례&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;과학 실험의 데이터처럼 근삿값을 다루는 영역에서는 이런 오차가 허용 범위 안에 드는 경우가 많습니다.
그러나 금액, 성적처럼 값이 정확히 같아야 하거나, 오차가 누적되는 계산 로직에서는 작은 오차도 치명적인 버그가 됩니다.
이런 오차가 실제 장애로 이어진 국내와 해외 사례를 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2011년_neis_성적_처리_오류&quot;&gt;3.1. 2011년 NEIS 성적 처리 오류&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2011년 7월 차세대 교육행정정보시스템(NEIS·나이스)에서 성적 처리 오류가 발생해, 전국 823개 고교에서 2만 9천여 명의 학기말 석차가 바뀌었습니다.
대입 수시모집을 앞둔 시점이어서 파장이 컸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;나이스는 지필고사와 수행평가 점수를 합해 한 학기 총점을 산출했습니다.
총점이 같은 학생들의 석차는 학교가 45가지 방법 중에서 고른 기준으로 정했습니다(&lt;a href=&quot;https://m.yeongnam.com/view.php?key=20110726.010061333590001&quot;&gt;영남일보 기사&lt;/a&gt;).
예를 들어 지필고사 점수를 우선하는 학교라면, 지필고사 65점에 수행평가 25점을 받은 A 학생과 지필고사 60점에 수행평가 30점을 받은 B 학생은 총점이 똑같이 90점이어도 A 학생이 앞선 등수를 받습니다.
동점자를 구분할 기준을 학교마다 미리 정해 두어야 할 만큼, 동점 판별은 석차 산출에서 중요한 절차입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;나이스는 총점을 소수점 이하 16자리까지 표시하도록 짜여 있었는데, 그 소수점 이하 자리에서 불규칙적으로 &apos;1&amp;#8217;이 나타나는 값이 발생했습니다.
글 앞부분에서 1.03 - 0.42의 결과로 0.6100000000000001이 나온 것과 같은 형태입니다.
예를 들어 두 학생의 총점이 90과 90.00000000000001로 계산되면 두 값은 같지 않으므로, 동점 여부를 그대로 비교하면 두 학생은 동점자가 아니고 오차가 붙은 쪽이 더 높은 점수로 정렬됩니다.
이 때문에 총점이 같아야 할 학생이 동점자로 판별되지 않거나 동점자 사이의 석차가 뒤집혔습니다.
(&lt;a href=&quot;https://use.go.kr/news/user/bbs/BD_selectBbs.do?q_bbsSn=1005&amp;amp;q_bbsDocNo=13244&quot;&gt;울산광역시교육청 안내문&lt;/a&gt;).
교육과학기술부는 이 계산 오차를 보정하지 않은 것이 버그의 원인이라고 설명했습니다(&lt;a href=&quot;https://www.khan.co.kr/article/201107222144165&quot;&gt;경향신문 기사&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 수를 더해도 더하는 순서에 따라 합이 달라지는 것을 jshell로 확인할 수 있습니다.
아래는 나이스의 실제 계산식이 아니라 원리를 보여 주는 예입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;더하는 순서에 따라 달라지는 합&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; 0.1 + 0.2 + 0.3
$1 ==&amp;gt; 0.6000000000000001

jshell&amp;gt; 0.3 + 0.2 + 0.1
$2 ==&amp;gt; 0.6

jshell&amp;gt; (0.1 + 0.2 + 0.3) == (0.3 + 0.2 + 0.1)
$3 ==&amp;gt; false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 세 수를 더했으므로 두 합은 같아야 하지만, &lt;code&gt;double&lt;/code&gt;로 계산하면 서로 다른 값이 되어 동점으로 판별되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;교육과학기술부 집계로는 전국 823개 고교에서 2만 9,007명의 석차가 바뀌었고, 그중 350개교 2,416명은 석차 등급까지 바뀌었습니다(&lt;a href=&quot;https://www.jejunews.com/news/articleView.html?idxno=957280&quot;&gt;제주일보 기사&lt;/a&gt;).
학교들은 오류 정정 프로그램으로 성적을 다시 계산하고 성적표를 재발송해야 했습니다(경향신문 기사).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;교육과학기술부 특별점검단이 같은 해 9월에 발표한 점검 결과에는 어떤 타입에서 오차가 났는지까지 나옵니다.
구 나이스의 프로그램을 차세대 나이스로 재개발하는 과정에서 &quot;새로 설치된 데이터베이스(DB2)의 특성상 나타나는 실수형(Double형) 자료 연산 오류를 예측하지 못하여 성적 처리 오류가 발생했던 것을 확인했다&quot;는 내용입니다(&lt;a href=&quot;https://www.etnews.com/201109030016&quot;&gt;전자신문 기사&lt;/a&gt;).
즉 성적은 DB2의 DOUBLE 타입으로 계산되었습니다.
&lt;a href=&quot;https://www.ibm.com/docs/en/db2/11.5?topic=list-numbers&quot;&gt;Linux, UNIX, Windows용 Db2&lt;/a&gt;의 DOUBLE 타입은 64비트이고 값의 범위가 IEEE 754 binary64와 같아서, 자바의 &lt;code&gt;double&lt;/code&gt;과 같은 방식으로 오차가 생깁니다.
그리고 그 오차를 바로잡는 &apos;오류 보정 코드&amp;#8217;가 일부 프로그램에 빠져 있어 그곳에서 석차가 뒤바뀐 것입니다(&lt;a href=&quot;https://www.yna.co.kr/view/AKR20110902079600004&quot;&gt;연합뉴스 기사&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;당시 네트워크 개발자 이경문 씨는 &lt;a href=&quot;https://www.dailysecu.com/news/articleView.html?idxno=319&quot;&gt;데일리시큐 기사&lt;/a&gt;에서 &quot;정확한 수치 표현을 해야 하는 곳에서 부동소수점을 이용한 것&quot;이 문제라며, 오차를 보정할 것이 아니라 정수로 처리하도록 설계를 바꾸거나 무한대의 정밀도를 지원하는 타입을 써야 한다고 지적했습니다.
이 글 뒷부분에서 소개하는 정수형 최소 단위와 &lt;code&gt;BigDecimal&lt;/code&gt;이 바로 그 두 방안입니다.
이 사건은 성적처럼 소수점 계산이 들어가는 값을 부동소수점으로 다루면 동점 여부 판정 같은 비교 연산에서 오류가 날 수 있음을 보여 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1991년_걸프전_패트리어트_미사일_요격_실패&quot;&gt;3.2. 1991년 걸프전 패트리어트 미사일 요격 실패&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;0.1을 유한한 2진수로 표현할 수 없다는 문제는 인명 피해로도 이어졌습니다.
걸프전 중이던 1991년 2월 25일, 사우디아라비아 다란(Dhahran)의 미군 기지에 이라크의 스커드 미사일이 떨어져 미군 28명이 사망하고 99명이 다쳤습니다(&lt;a href=&quot;https://www.dvidshub.net/news/525747/scud-alert-after-blast&quot;&gt;미 육군 기사&lt;/a&gt;).
기지에는 요격용 패트리어트 미사일이 배치되어 있었지만 격추 시도조차 하지 못했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;역사 속의 소프트웨어 오류&apos;(에이콘출판, 2014)의 1장 &quot;0.000000095의 오차가 앗아간 28명의 생명&quot;은 미국 GAO의 &lt;a href=&quot;https://www.gao.gov/products/imtec-92-26&quot;&gt;조사 보고서&lt;/a&gt;를 바탕으로 이 사건의 원인을 설명합니다.
미사일을 방어하는 패트리어트 시스템은 0.1초 단위로 시간을 세어 목표물이 나타날 구간을 예측하는데, 센 횟수를 초로 바꿀 때 24비트 고정소수점 레지스터에 담은 0.1을 곱했습니다.
0.1은 2진수로는 무한히 반복되는 소수이므로 24비트 이후는 잘려 나갔고, 0.1초가 지날 때마다 약 0.000000095초의 오차가 쌓였습니다.
연속 가동 20시간이면 계산한 시각이 실제보다 0.0687초, 사건 당시 포대처럼 약 100시간이면 0.3433초 작아집니다.
GAO 보고서의 계산으로는 이 시간 오차 때문에 목표물을 찾는 구간이 687m 어긋납니다.
추적 레이더는 실제 미사일 위치에서 그만큼 벗어난 구간을 탐색했습니다.
레이더가 그 구간에서 목표물을 찾지 못해 요격 미사일은 발사되지 않았고, 스커드 미사일은 병영에 떨어졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 시스템은 &lt;code&gt;double&lt;/code&gt; 같은 부동소수점이 아니라 고정소수점으로 계산했습니다.
그러나 0.1을 유한한 비트의 2진수에 담지 못해 오차가 누적되었다는 점은 이 글에서 다룬 원리와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;자바에서_소수점_연산_오차를_피하는_대안&quot;&gt;4. 자바에서 소수점 연산 오차를 피하는 대안&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;double&lt;/code&gt;형 연산의 오차를 피하는 대안으로 &lt;code&gt;BigDecimal&lt;/code&gt;을 쓰는 방법과 금액을 최소 단위의 정수로 다루는 방법을 소개합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;bigdecimal로_계산하기&quot;&gt;4.1. BigDecimal로 계산하기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자바에서 십진 소수를 정확하게 다루는 기본 수단은 &lt;code&gt;java.math.BigDecimal&lt;/code&gt;입니다.
&lt;a href=&quot;https://dzone.com/articles/never-use-float-and-double-for-monetary-calculatio&quot;&gt;Why You Should Never Use Float and Double for Monetary Calculations&lt;/a&gt;에서도 금액 계산에 부동소수점 자료형 대신 &lt;code&gt;BigDecimal&lt;/code&gt;을 쓰라고 권고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;BigDecimal의 더하기 빼기 연산&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; new BigDecimal(&quot;0.1&quot;).add(new BigDecimal(&quot;0.2&quot;))
$1 ==&amp;gt; 0.3

jshell&amp;gt; new BigDecimal(&quot;1.03&quot;).subtract(new BigDecimal(&quot;0.42&quot;))
$2 ==&amp;gt; 0.61&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주의할 점이 하나 있습니다. 생성자에 &lt;code&gt;double&lt;/code&gt;을 넘기면 십진 소수를 &lt;code&gt;double&lt;/code&gt;에 담을 때 생긴 반올림 오차가 &lt;code&gt;BigDecimal&lt;/code&gt;에 그대로 전달됩니다.
앞에서 봤듯이 &lt;code&gt;new BigDecimal(0.1)&lt;/code&gt;은 0.1이 아니라 &lt;code&gt;double&lt;/code&gt;에 저장된 근삿값을 옮겨 담습니다.
이 오차를 피하려면 문자열을 받는 생성자나 &lt;code&gt;BigDecimal.valueOf()&lt;/code&gt;를 사용해야 합니다.
&lt;code&gt;BigDecimal.valueOf(0.1)&lt;/code&gt;은 &lt;code&gt;Double.toString(0.1)&lt;/code&gt;이 돌려주는 문자열 &quot;0.1&quot;로 값을 만들므로 0.1이 됩니다.
다만 &lt;code&gt;valueOf()&lt;/code&gt;는 &lt;code&gt;double&lt;/code&gt;로 이미 계산한 값의 오차까지 없애지는 못합니다.
&lt;code&gt;BigDecimal.valueOf(0.1 + 0.2)&lt;/code&gt;는 0.30000000000000004가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;BigDecimal&lt;/code&gt;을 쓴다고 반올림 정책까지 저절로 정해지지는 않습니다.
1을 3으로 나누는 것처럼 십진수로 끝나지 않는 나눗셈은 자릿수와 반올림 방식을 지정하지 않으면 예외가 발생합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; new BigDecimal(&quot;1&quot;).divide(new BigDecimal(&quot;3&quot;))
|  Exception java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

jshell&amp;gt; new BigDecimal(&quot;1&quot;).divide(new BigDecimal(&quot;3&quot;), 10, RoundingMode.HALF_UP)
$1 ==&amp;gt; 0.3333333333&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서 쓴 &lt;code&gt;java.math.RoundingMode&lt;/code&gt;는 0.5를 올릴지(&lt;code&gt;HALF_UP&lt;/code&gt;), 가장 가까운 짝수로 보낼지(&lt;code&gt;HALF_EVEN&lt;/code&gt;), 무조건 버릴지(&lt;code&gt;DOWN&lt;/code&gt;) 같은 반올림 방식을 고르는 열거형입니다.
예를 들어 2.5와 3.5를 정수로 만들면 &lt;code&gt;HALF_UP&lt;/code&gt;은 3과 4, &lt;code&gt;HALF_EVEN&lt;/code&gt;은 2와 4, &lt;code&gt;DOWN&lt;/code&gt;은 2와 3이 됩니다.
&lt;code&gt;BigDecimal&lt;/code&gt;이 없애 주는 것은 2진수 표현에서 생기는 오차입니다.
소수점 이하 몇 자리까지 어떤 방식으로 반올림할지는 해당 분야의 특성에 맞게 팀이 정해서 지켜야 하는 규칙이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Joshua Bloch도 &apos;Effective Java&apos; 3판의 아이템 60 &apos;정확한 답이 필요하다면 float와 double은 피하라&apos;(Avoid float and double if exact answers are required)에서 같은 권고를 합니다.
이 항목은 &lt;code&gt;BigDecimal&lt;/code&gt;과 함께 &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;도 대안으로 제시하는데, 둘 중 무엇을 고를지는 뒤의 선택 기준 절에서 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;정수로_계산하기&quot;&gt;4.2. 정수로 계산하기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;금액을 원(₩)이나 센트 같은 최소 단위의 정수로 다루는 방법도 널리 쓰입니다.
결제 서비스인 Stripe의 API가 그런 예입니다.
&lt;a href=&quot;https://docs.stripe.com/api/charges/object&quot;&gt;Charge 객체&lt;/a&gt;의 &lt;code&gt;amount&lt;/code&gt; 속성은 integer 타입이고, 설명은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A positive integer representing how much to charge in the smallest currency unit (e.g., 100 cents to charge $1.00 or 100 to charge ¥100, a zero-decimal currency).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;청구할 금액을 최소 통화 단위로 나타낸 양의 정수(예: $1.00를 청구하려면 100센트, 소수 단위가 없는 통화인 엔화로 ¥100을 청구하려면 100).&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; Stripe API Reference&lt;br&gt;
&lt;cite&gt;The Charge object&lt;/cite&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;특히 1원 이하의 가치가 적은 한국에서는 업무 규칙과 어긋나지 않는다면 실용적인 방법입니다.
같은 이유로 기존 시스템의 데이터베이스도 원화 금액을 정수 타입 칼럼으로 정의한 경우가 많습니다.
그래서 애플리케이션에서 &lt;code&gt;BigDecimal&lt;/code&gt;로 계산하더라도 저장하기 직전에 정수로 바꾸는 과정이 한 번은 들어갑니다.
우아한형제들 기술 블로그의 &lt;a href=&quot;https://techblog.woowahan.com/2560/&quot;&gt;Spock으로 테스트코드를 짜보자&lt;/a&gt;에 나오는 예제가 이 방식을 보여 줍니다.
반올림 처리에는 &lt;code&gt;BigDecimal&lt;/code&gt;을 쓰지만, 금액 파라미터 &lt;code&gt;amount&lt;/code&gt;와 리턴 타입은 &lt;code&gt;long&lt;/code&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static long calculate(long amount, float rate, RoundingMode roundingMode) {
    return BigDecimal.valueOf(amount * rate * 0.01)
            .setScale(0, roundingMode).longValue();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;금액 자체는 정수로 유지하고, 소수점이 생기는 비율 계산 직후에 반올림 모드를 명시한 &lt;code&gt;BigDecimal&lt;/code&gt;로 다시 정수로 돌아오는 구조입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 &lt;code&gt;amount * rate * 0.01&lt;/code&gt;처럼 부동소수점 자료형이 섞이면 그 계산은 부동소수점 연산이 됩니다.
&lt;code&gt;long&lt;/code&gt; 값이 &lt;code&gt;float&lt;/code&gt;로 변환되고 곱셈도 &lt;code&gt;float&lt;/code&gt;로 계산되면서 정밀도를 잃기 때문에, 금액 파라미터와 리턴 타입이 정수여도 오차가 생길 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;calculate 메서드의 한계&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; calculate(16_777_217L, 100f, RoundingMode.HALF_UP)
$1 ==&amp;gt; 16777216

jshell&amp;gt; calculate(671_089L, 50f, RoundingMode.HALF_UP)
$2 ==&amp;gt; 335544&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 번째 호출은 100%를 곱했으니 16,777,217이 그대로 나와야 하는데 1원이 줄었습니다.
&lt;code&gt;float&lt;/code&gt;는 가수 23비트에 생략된 1비트를 더해 24비트 정밀도를 가지므로 2&lt;sup&gt;24&lt;/sup&gt;(16,777,216)을 넘는 정수를 빠짐없이 표현하지는 못하고, &lt;code&gt;16_777_217L&lt;/code&gt;은 &lt;code&gt;float&lt;/code&gt;로 변환되는 순간 16,777,216이 됩니다.
두 번째 호출은 671,089원의 50%인 335,544.5원을 반올림한 335,545가 나와야 하는데 1원이 모자랍니다.
금액은 2&lt;sup&gt;24&lt;/sup&gt;보다 작지만, 비율을 곱한 값 33,554,450이 2&lt;sup&gt;24&lt;/sup&gt;을 넘어 &lt;code&gt;float&lt;/code&gt;에는 33,554,448로 저장되기 때문입니다.
입력을 정수로 유지해도 중간 계산 한 곳이 부동소수점이면 오차를 피하지 못합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인용한 예제를 고쳐서 비율까지 &lt;code&gt;BigDecimal&lt;/code&gt;로 옮기면 16,777,217이 그대로 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;곱셈까지 BigDecimal로 옮긴 버전&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static long calculate(long amount, BigDecimal percent, RoundingMode roundingMode) {
    return BigDecimal.valueOf(amount)
            .multiply(percent)
            .movePointLeft(2)
            .setScale(0, roundingMode)
            .longValueExact();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;고친 calculate 메서드의 실행 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;jshell&amp;gt; calculate(16_777_217L, new BigDecimal(&quot;100&quot;), RoundingMode.HALF_UP)
$1 ==&amp;gt; 16777217&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마지막 줄의 &lt;code&gt;longValueExact()&lt;/code&gt;는 결과가 &lt;code&gt;long&lt;/code&gt; 범위를 넘으면 &lt;code&gt;ArithmeticException&lt;/code&gt;을 던집니다.
&lt;code&gt;longValue()&lt;/code&gt;는 이럴 때 예외 없이 하위 64비트만 돌려주므로 틀린 금액이 그대로 저장될 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정수형 최소 단위 방식을 쓰더라도 오차를 없애려면 모든 중간값을 정수나 &lt;code&gt;BigDecimal&lt;/code&gt;로 유지해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;bigdecimal과_정수_계산의_선택_기준&quot;&gt;5. BigDecimal과 정수 계산의 선택 기준&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;계산 정밀도가 중요한 모듈이라면 적어도 메서드 파라미터와 리턴 타입에는 &lt;code&gt;double&lt;/code&gt;과 &lt;code&gt;float&lt;/code&gt;를 쓰지 않는 것이 좋습니다.
대신 쓸 타입으로는 앞에서 본 &lt;code&gt;BigDecimal&lt;/code&gt;과 정수형 최소 단위(&lt;code&gt;long&lt;/code&gt;) 두 가지가 있습니다.
성능과 코드의 간결함은 &lt;code&gt;long&lt;/code&gt;이 낫습니다.
&lt;code&gt;BigDecimal&lt;/code&gt;은 개발자의 실수를 예방하는 측면에서는 유리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 언급한 &apos;Effective Java&apos; 아이템 60도 같은 기준으로 선택을 정리해 두었습니다.
소수점 추적을 시스템에 맡기고 반올림 방식까지 고르고 싶다면, 기본 타입보다 쓰기 불편하고 느리다는 비용을 감수하고 &lt;code&gt;BigDecimal&lt;/code&gt;을 쓰라는 것이 이 항목의 결론입니다.
반대로 성능이 중요하고 소수점을 직접 추적할 수 있다면 &lt;code&gt;int&lt;/code&gt;나 &lt;code&gt;long&lt;/code&gt;을 쓰라는 대안도 함께 제시하는데, 앞 절에서 본 정수로 계산하는 방법과 같습니다.
값이 십진수로 아홉 자리를 넘지 않으면 &lt;code&gt;int&lt;/code&gt;, 열여덟 자리를 넘지 않으면 &lt;code&gt;long&lt;/code&gt;, 그보다 크면 &lt;code&gt;BigDecimal&lt;/code&gt;을 쓰라는 자릿수 기준도 덧붙입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Chronicle Software의 Peter Lawrey는 &lt;a href=&quot;https://blog.vanillajava.blog/2014/07/if-bigdecimal-is-answer-it-must-have.html&quot;&gt;If BigDecimal is the answer, it must have been a strange question&lt;/a&gt;에서 &lt;code&gt;BigDecimal&lt;/code&gt;의 단점을 이렇게 꼽습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;문법이 자연스럽지 않다. 연산자 대신 메서드 호출을 이어 붙여야 해서 수식의 가독성이 떨어진다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;double&lt;/code&gt;보다 메모리를 많이 사용한다. 객체를 계속 생성하므로 가비지가 많이 만들어진다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;대부분의 연산에서 훨씬 느리다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글에는 속도 차이를 잰 JMH 벤치마크도 실려 있습니다.
두 값의 평균을 구해 소수점 아래 6자리로 반올림하는 계산을 원소 1,024개짜리 배열에 반복한 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 1. Lawrey의 JMH 벤치마크 결과&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;벤치마크&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;처리량(ops/s)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;측정 오차&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;doubleMidPrice&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;123,208.083&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;±2,109.738&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;bigDecimalMidPrice&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;23,638.568&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;±590.094&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;double&lt;/code&gt;로 구현한 쪽의 처리량이 &lt;code&gt;BigDecimal&lt;/code&gt;의 약 5.2배입니다.
2014년에 측정한 값이므로 지금의 JVM에서는 수치가 다를 수 있습니다.
&lt;code&gt;double&lt;/code&gt;과 비교한 결과이고, &lt;code&gt;long&lt;/code&gt;과 직접 비교한 것은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lawrey의 결론은 &quot;&lt;code&gt;double&lt;/code&gt;의 반올림을 다루는 법을 모르거나 프로젝트 표준이 &lt;code&gt;BigDecimal&lt;/code&gt;을 강제한다면 &lt;code&gt;BigDecimal&lt;/code&gt;을 쓰되, 선택할 수 있다면 &lt;code&gt;BigDecimal&lt;/code&gt;이 당연히 옳다고 가정하지는 말라&quot;는 것입니다.
금융 애플리케이션의 성능 개선을 맡으면 언젠가는 &lt;code&gt;BigDecimal&lt;/code&gt;을 걷어 내게 된다는 자신의 경험도 근거로 듭니다.
&lt;code&gt;BigDecimal&lt;/code&gt;이 처음부터 가장 큰 지연 원인은 아니지만, 다른 병목을 고쳐 나가다 보면 결국 가장 느린 부분으로 올라온다는 것입니다.
댓글에서는 트레이딩·금융 시스템이 &lt;code&gt;BigDecimal&lt;/code&gt;보다 먼저 있었고, 십진 소수 타입 없이 C++로 만든 시스템도 많다고 덧붙입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정수형 최소 단위 계산은 시스템 자원을 적게 쓴다는 장점이 있습니다.
&lt;code&gt;long&lt;/code&gt; 타입의 값은 객체를 만들지 않고, 연산도 CPU의 정수 연산으로 끝납니다.
메모리 크기는 &lt;a href=&quot;https://github.com/openjdk/jol&quot;&gt;JOL(Java Object Layout)&lt;/a&gt;로 잴 수 있습니다.
JDK 25 HotSpot 64비트 기본 설정에서 &lt;code&gt;new BigDecimal(&quot;1.03&quot;)&lt;/code&gt; 객체는 40바이트입니다.
값이 &lt;code&gt;long&lt;/code&gt; 범위를 넘으면 내부에 &lt;code&gt;BigInteger&lt;/code&gt;와 &lt;code&gt;int&lt;/code&gt; 배열이 추가되어, 22자리 수인 &lt;code&gt;new BigDecimal(&quot;12345678901234567890.12&quot;)&lt;/code&gt;는 모두 112바이트를 씁니다.
&lt;code&gt;long&lt;/code&gt; 값은 8바이트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대신 &lt;code&gt;long&lt;/code&gt; 방식은 중간에 나눗셈이 있거나 &lt;code&gt;double&lt;/code&gt;이나 &lt;code&gt;float&lt;/code&gt;가 섞인 연산이 있으면 개발자가 정확성을 더 많이 의식해야 합니다.
반올림 규칙을 정해야 하는 것은 &lt;code&gt;BigDecimal&lt;/code&gt;도 마찬가지이지만, &lt;code&gt;long&lt;/code&gt; 방식에서는 그 규칙을 빠뜨렸을 때 생기는 오류를 알아차리기가 더 어렵습니다.
&lt;code&gt;BigDecimal&lt;/code&gt;은 십진수로 끝나지 않는 나눗셈에서 반올림 방식을 빠뜨리면 앞에서 본 것처럼 예외를 던지지만, &lt;code&gt;long&lt;/code&gt; 나눗셈은 소수점 이하를 경고 없이 버립니다. 예를 들어 &lt;code&gt;10 / 3&lt;/code&gt;은 3이 됩니다.
또 기본 산술 연산자로 계산하므로, &lt;code&gt;calculate()&lt;/code&gt; 예제의 &lt;code&gt;amount * rate * 0.01&lt;/code&gt;처럼 부동소수점이 섞여도 컴파일러가 막지 않습니다.
최소 단위를 무엇으로 볼지 시스템 전체가 같은 규칙으로 지켜야 한다는 점도 &lt;code&gt;BigDecimal&lt;/code&gt;에는 없는 부담입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지의 내용을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 2. BigDecimal과 정수 계산 비교&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3335%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;자료형&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;저장 방식&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;강점&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;감수할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;BigDecimal&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;임의 정밀도 정수와 스케일&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;div class=&quot;content&quot;&gt;&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;자릿수를 필요한 만큼 늘려 십진수를 정확하게 표현&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;정수로 환산할 자릿수를 따로 정하지 않아도 됨&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;div class=&quot;content&quot;&gt;&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;메모리 사용량 증가(앞의 측정에서는 객체 하나에 40바이트 이상)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;연산 속도 저하&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;기본 연산자 대신 메서드 체인 호출로 연산해야 하는 번거로움&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;long&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;최소 단위로 환산한 정수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;div class=&quot;content&quot;&gt;&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;객체 생성이 없어 메모리 사용이 적음&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CPU의 정수 연산으로 끝나 빠름&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;div class=&quot;content&quot;&gt;&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;중간 연산에 &lt;code&gt;double&lt;/code&gt;이나 &lt;code&gt;float&lt;/code&gt;가 끼어들면 오차가 생김&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;최소 단위 규칙을 시스템 전체가 지켜야 함&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 타입을 쓸지 정했다면, 팀 구성원 누구나 이해할 수 있는 단순한 규칙으로 문서에 적어야 합니다.
예를 들면 &quot;금액은 모든 계층에서 &lt;code&gt;long&lt;/code&gt;(원 단위)으로 다루고, 비율 계산만 &lt;code&gt;BigDecimal&lt;/code&gt;로 한 뒤 &lt;code&gt;RoundingMode.DOWN&lt;/code&gt;으로 버린다&quot; 같은 규칙입니다.
시스템의 담당 개발자는 계속 바뀔 수 있습니다.
모든 사람이 이 글에서 다룬 오차가 생기는 원리까지 이해하고 있다고 전제하기는 어렵습니다.
규칙이 명확하고 단순하면 새로 투입된 개발자도 같은 방식으로 따를 가능성이 높아집니다.
그 결과로 전체 모듈에서 소수점 이하 연산을 다루는 일관성이 지켜지고, 치명적인 오차를 만드는 코드가 들어올 가능성도 줄어듭니다.
따라서 정하려는 규칙을 간결하고 명확하게 문서로 적을 수 있는지도 판단 기준으로 삼아 봤으면 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;규칙을 정하고 나면 그와 어긋나는 기존 코드까지 고쳐야 앞으로도 일관성이 유지됩니다.
그런데 이미 잘 돌아가고 있는 데다 금액을 직접 다루는 코드는 고치기가 무섭습니다.
그래서 한번 자리 잡은 방식을 바꾸지 않는 경우도 많습니다.
기존 코드를 고쳐야 할 때를 대비해서라도, 금액처럼 민감한 수치를 다루는 로직은 앞의 &lt;code&gt;calculate()&lt;/code&gt;처럼 별도 메서드로 분리해 두어야 합니다.
그러면 데이터베이스나 외부 시스템 없이 입력과 기대 결과만으로 테스트할 수 있습니다.
테스트하기 쉬운 구조라면 소수점 오차뿐 아니라 새로 추가하는 코드의 논리 오류도 이른 시점에 감지할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;금액과_단위를_다루는_도메인_특화_라이브러리&quot;&gt;6. 금액과 단위를 다루는 도메인 특화 라이브러리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 본 두 방법은 수를 어떤 형태로 담을지의 선택입니다.
그 위에서 금액, 시간, 물리량처럼 단위가 붙는 값을 전용 타입으로 감싸는 도메인 특화 라이브러리도 있습니다.
금액을 다루는 라이브러리는 안에서 &lt;code&gt;BigDecimal&lt;/code&gt;이나 정수형 최소 단위 중 하나로 수를 담습니다.
대표적인 예로 Java Money를 살펴봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;java_money_jsr_354&quot;&gt;6.1. Java Money (JSR 354)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://javamoney.github.io/&quot;&gt;Java Money&lt;/a&gt;는 금액과 통화를 함께 다루는 API로, JSR 354로 표준화됐습니다.
&lt;code&gt;MonetaryAmount&lt;/code&gt; 인터페이스로 금액과 통화 단위를 묶어서 표현합니다.
&lt;a href=&quot;https://jcp.org/en/jsr/detail?id=354&quot;&gt;JCP 제안서&lt;/a&gt;에서는 Java SE 9에 포함하는 방안도 검토했지만 최종적으로 JDK에는 들어가지 않았습니다.
그래서 API(&lt;code&gt;javax.money&lt;/code&gt;)와 참조 구현인 &lt;a href=&quot;https://github.com/JavaMoney/jsr354-ri&quot;&gt;Moneta&lt;/a&gt;를 별도 의존성으로 추가해야 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;implementation &apos;org.javamoney:moneta:1.4.5&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Money&lt;/code&gt; 구현체는 내부적으로 &lt;code&gt;BigDecimal&lt;/code&gt;로 계산하므로, 글 앞부분에서 &lt;code&gt;double&lt;/code&gt;로는 0.6100000000000001이 나왔던 1.03 - 0.42도 정확히 0.61이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;MonetaryAmount price = Money.of(new BigDecimal(&quot;1.03&quot;), &quot;USD&quot;);
MonetaryAmount result = price.subtract(Money.of(new BigDecimal(&quot;0.42&quot;), &quot;USD&quot;));
System.out.println(result); // USD 0.61&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;통화가 다른 금액끼리 연산하면 &lt;code&gt;javax.money.MonetaryException&lt;/code&gt;이 발생합니다.
서로 다른 통화의 금액을 섞어 계산하는 실수를 API가 실행 시점에 예외로 막아 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;price.add(Money.of(100, &quot;KRW&quot;)); // MonetaryException: Currency mismatch: USD/KRW&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java Money는 앞의 두 방법을 대체하지 않고 그 위에 얹혀 있습니다.
Moneta가 제공하는 &lt;code&gt;MonetaryAmount&lt;/code&gt; 구현체 중 대표적인 두 가지가 이 글에서 본 두 가지 수치 표현에 그대로 대응합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Money&lt;/code&gt;: 금액을 &lt;code&gt;BigDecimal&lt;/code&gt;로 저장합니다. 그만큼 앞에서 정리한 &lt;code&gt;BigDecimal&lt;/code&gt;의 비용도 그대로 집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JavaMoney/jsr354-ri/blob/master/moneta-core/src/main/java/org/javamoney/moneta/FastMoney.java&quot;&gt;FastMoney&lt;/a&gt;: 금액을 &lt;code&gt;long&lt;/code&gt; 하나에 최소 단위로 저장합니다. 소수점 아래 5자리로 스케일이 고정되므로 표현할 수 있는 최대 금액은 약 92조(&lt;code&gt;Long.MAX_VALUE&lt;/code&gt; / 10&lt;sup&gt;5&lt;/sup&gt;)입니다. 이런 제약이 있는 대신 빠르고, 자바독은 &lt;code&gt;Money&lt;/code&gt;보다 10~15배 빠르다고 소개합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인터페이스가 같으므로 속도가 문제가 되면 &lt;code&gt;Money&lt;/code&gt; 대신 &lt;code&gt;FastMoney&lt;/code&gt;로 바꿔 끼울 수 있습니다.
도메인 타입을 도입하더라도 수치 표현을 무엇으로 할지는 여전히 정해야 하고, 앞 절에서 정리한 트레이드오프가 구현체 선택으로 다시 나타납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java Money를 선택할지는 통화를 여러 개 다루는지에 달렸다고 봅니다.
통화가 하나뿐이라면 &lt;code&gt;BigDecimal&lt;/code&gt;이나 &lt;code&gt;long&lt;/code&gt; 값을 그대로 쓰는 것으로도 충분하고, 여러 통화가 섞이는 시스템이라면 통화 불일치를 API가 걸러 준다는 이점이 별도 의존성을 감수할 만합니다.
더 많은 사용 예제는 &lt;a href=&quot;https://www.baeldung.com/java-money-and-currency&quot;&gt;Baeldung의 Java Money and the Currency API&lt;/a&gt;를 참고할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;그_밖의_도메인_특화_라이브러리&quot;&gt;6.2. 그 밖의 도메인 특화 라이브러리&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;수치 표현을 도메인 타입으로 감싸는 라이브러리는 Java Money 외에도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.joda.org/joda-money/&quot;&gt;Joda-Money&lt;/a&gt;: Java Money 이전부터 쓰인 금액 라이브러리입니다. &lt;code&gt;Money&lt;/code&gt; 클래스는 금액을 &lt;code&gt;BigDecimal&lt;/code&gt;로 저장하되 스케일을 통화의 기본 소수 자릿수(달러는 2자리, 엔은 0자리)로 고정하고, &lt;code&gt;BigMoney&lt;/code&gt; 클래스는 스케일을 자유롭게 둡니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://unitsofmeasurement.github.io/&quot;&gt;Units of Measurement API (JSR 385)&lt;/a&gt;: 길이나 질량 같은 물리량과 그 단위를 타입으로 표현합니다. API 패키지는 &lt;code&gt;javax.measure&lt;/code&gt;이고 참조 구현은 &lt;a href=&quot;https://unitsofmeasurement.github.io/indriya/&quot;&gt;Indriya&lt;/a&gt;입니다. 금액에서 통화가 다르면 예외가 나는 것처럼 물리량의 종류가 맞지 않는 계산을 걸러내는데, 제네릭 타입이 정해져 있으면 컴파일 시점에 막습니다. 예를 들어 &lt;code&gt;Quantity&amp;lt;Length&amp;gt;&lt;/code&gt;에 &lt;code&gt;Quantity&amp;lt;Mass&amp;gt;&lt;/code&gt;를 더하는 코드는 제네릭 타입이 달라 컴파일되지 않습니다. 다만 단위 오류를 막는 것과 수치를 정확하게 계산하는 것은 별개입니다. 값을 &lt;code&gt;Number&lt;/code&gt;로 받으므로 &lt;code&gt;double&lt;/code&gt;로 만든 물리량에는 부동소수점 오차가 그대로 남습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/Duration.html&quot;&gt;java.time.Duration&lt;/a&gt;: 별도 의존성이 아니라 JDK 안에 있는 예입니다. 시간의 길이를 초(&lt;code&gt;long&lt;/code&gt;)와 나노초(&lt;code&gt;int&lt;/code&gt;) 두 필드로 나눠 저장합니다. 소수점 있는 초를 &lt;code&gt;double&lt;/code&gt; 하나로 담지 않는다는 점에서 정수형 최소 단위와 같은 기법입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 목록에 맞는 라이브러리가 없더라도, 시스템 안에서 직접 만든 타입으로 같은 역할을 할 수 있습니다.
이때도 수를 &lt;code&gt;BigDecimal&lt;/code&gt;과 정수 중 무엇으로 담을지는 이 글에서 설명한 기준에 따라 정합니다.
그리고 중간 연산에 &lt;code&gt;float&lt;/code&gt;나 &lt;code&gt;double&lt;/code&gt;이 끼어들어 생기는 오류를 막아야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;7. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/IEEE_754&quot;&gt;IEEE 754 - 위키백과&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Joshua Bloch, &lt;a href=&quot;https://www.informit.com/store/effective-java-9780134685991&quot;&gt;&lt;strong&gt;Effective Java&lt;/strong&gt;&lt;/a&gt; 3판, Addison-Wesley, 2018, Item 60&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dzone.com/articles/never-use-float-and-double-for-monetary-calculatio&quot;&gt;Why You Should Never Use Float and Double for Monetary Calculations&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/bigdecimal-class-java/&quot;&gt;BigDecimal Class in Java - GeeksforGeeks&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.vanillajava.blog/2014/07/if-bigdecimal-is-answer-it-must-have.html&quot;&gt;If BigDecimal is the answer, it must have been a strange question - Peter Lawrey&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://techblog.woowahan.com/2560/&quot;&gt;Spock으로 테스트코드를 짜보자 - 우아한형제들 기술 블로그&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.stripe.com/api/charges/object&quot;&gt;The Charge object - Stripe API Reference&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jol&quot;&gt;JOL (Java Object Layout)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 id=&quot;장애_사례&quot; class=&quot;discrete&quot;&gt;장애 사례&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;NEIS 성적 오류&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.khan.co.kr/article/201107222144165&quot;&gt;“NEIS 성적 오류 어쩌다 발생했나” - 경향신문 (2011.07.22)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://m.yeongnam.com/view.php?key=20110726.010061333590001&quot;&gt;나이스 프로그램에 문제 &apos;쓰레기 값&apos; 처리 누락 - 영남일보 (2011.07.26)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dailysecu.com/news/articleView.html?idxno=319&quot;&gt;차세대 골칫거리 &apos;나이스&apos;, 무엇이 문제였나 - 데일리시큐 (2011.07.26)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yna.co.kr/view/AKR20110902079600004&quot;&gt;&apos;나이스오류&apos; 예견된 사고…설계서ㆍ테스트 없었다 - 연합뉴스 (2011.09.02)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.etnews.com/201109030016&quot;&gt;교육행정정보 시스템 &apos;나이스&apos; 개발 부실…삼성SDS 상대 소송 고려 - 전자신문 (2011.09.03)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;패트리어트 미사일 요격 실패&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;김종하, &lt;a href=&quot;https://acornpub.co.kr/product/%EC%97%AD%EC%82%AC-%EC%86%8D%EC%9D%98-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%98%A4%EB%A5%98/4533/&quot;&gt;&apos;역사 속의 소프트웨어 오류&apos;&lt;/a&gt;, 에이콘출판(2014), 25~35쪽&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.gao.gov/products/imtec-92-26&quot;&gt;Patriot Missile Defense: Software Problem Led to System Failure at Dhahran, Saudi Arabia - GAO (1992.02)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.dvidshub.net/news/525747/scud-alert-after-blast&quot;&gt;Scud Alert: After the Blast - DVIDS&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 id=&quot;도메인_특화_라이브러리&quot; class=&quot;discrete&quot;&gt;도메인 특화 라이브러리&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://javamoney.github.io/&quot;&gt;Java Money&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baeldung.com/java-money-and-currency&quot;&gt;Java Money and the Currency API - Baeldung&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.joda.org/joda-money/&quot;&gt;Joda-Money&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://unitsofmeasurement.github.io/&quot;&gt;Units of Measurement API (JSR 385)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>BTrace로 MySQL JDBC 드라이버의 내부 동작 측정하기</title>
      <link>https://blog.benelog.net//btrace-jdbc-monitoring.html</link>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">btrace-jdbc-monitoring.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#btrace_소개&quot;&gt;BTrace 소개&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#무엇을_측정하는가&quot;&gt;무엇을 측정하는가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#스크립트_구조&quot;&gt;스크립트 구조&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#실행_방법&quot;&gt;실행 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_25_기준의_선택&quot;&gt;JDK 25 기준의 선택&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#btrace를_계속_쓴다면&quot;&gt;BTrace를 계속 쓴다면&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_25라면_jfr만으로_가능&quot;&gt;JDK 25라면 JFR만으로 가능&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#그_밖의_대안&quot;&gt;그 밖의 대안&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BTrace로 실행 중인 애플리케이션의 MySQL JDBC 드라이버(Connector/J) 내부 메서드의 호출 횟수와 평균 실행 시간을 집계할 수 있습니다.
이를 수행하는 스크립트는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/btrace-scripts&quot;&gt;examples/btrace-scripts&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;btrace_소개&quot;&gt;BTrace 소개&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BTrace는 실행 중인 JVM에 붙어서(attach) 지정한 메서드에 계측 코드를 끼워 넣는 동적 트레이싱 도구입니다. 애플리케이션 소스를 수정하거나 서버를 재시작하지 않아도 됩니다. 추적할 지점과 수집할 값은 Java 문법으로 작성한 스크립트에 애너테이션으로 선언합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 블로그에서는 예전에 &lt;a href=&quot;2855201.html&quot;&gt;Btrace로 DBCP의 connection정보를 모니터링 하기&lt;/a&gt;에서 소개한 적이 있습니다. 이번 예제는 같은 도구로 Connection pool 대신 JDBC 드라이버 내부를 들여다봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;무엇을_측정하는가&quot;&gt;무엇을 측정하는가&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/btrace-scripts/src/main/java/ConnectionMonitor.java&quot;&gt;ConnectionMonitor.java&lt;/a&gt;는 MySQL Connector/J 5.1의 내부 메서드 4개를 추적합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;추적 대상&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;ConnectionImpl.prepareStatement()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;PreparedStatement&lt;/code&gt; 생성. 드라이버의 &lt;code&gt;cachePrepStmts&lt;/code&gt; 옵션의 효과를 확인할 수 있습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;ConnectionImpl.setAutoCommit()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;autoCommit 변경. 옵션에 따라 DB 서버와의 통신이 발생하는 지점입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;PreparedStatement.executeInternal()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;PreparedStatement&lt;/code&gt; 실행이 공통적으로 거치는 내부 메서드입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;ConnectionImpl.execSQL()&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;드라이버가 실제로 SQL을 서버에 전송하는 내부 메서드입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 메서드의 호출 횟수와 평균 실행 시간을 모으면, 드라이버 튜닝 옵션(&lt;code&gt;cachePrepStmts&lt;/code&gt;, &lt;code&gt;useLocalSessionState&lt;/code&gt; 등)을 바꿨을 때 내부 호출이 실제로 줄어드는지 수치로 확인할 수 있습니다. 애플리케이션 코드 계층에서는 보이지 않는 드라이버 내부의 동작이라서 BTrace 같은 도구가 유용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스크립트_구조&quot;&gt;스크립트 구조&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;핵심 부분만 옮기면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ConnectionMonitor.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@BTrace
public class ConnectionMonitor {
	private static Aggregation prepareDuration = Aggregations.newAggregation(AggregationFunction.AVERAGE);
	private static Aggregation prepareCount = Aggregations.newAggregation(AggregationFunction.COUNT);

	@OnMethod(clazz = &quot;com.mysql.jdbc.ConnectionImpl&quot;, method = &quot;prepareStatement&quot;, location = @Location(Kind.RETURN))
	public static void statementPrepare(@Duration long duration) {
		Aggregations.addToAggregation(prepareDuration, duration / 1000);
		Aggregations.addToAggregation(prepareCount, 1);
	}

	@OnEvent
	public static void summary() {
		println(&quot;## com.mysql.jdbc.ConnectionImp.prepareStatement()&quot;);
		Aggregations.printAggregation(&quot;- call count : &quot;, prepareCount);
		Aggregations.printAggregation(&quot;- average duration(microseconds) :&quot;, prepareDuration);
		Sys.exit(0);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;사용한 BTrace 기능은 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@OnMethod&lt;/code&gt; + &lt;code&gt;@Location(Kind.RETURN)&lt;/code&gt; : 지정한 클래스·메서드가 리턴하는 시점에 핸들러를 실행합니다. 메서드 실행 시간을 담는 &lt;code&gt;@Duration&lt;/code&gt; 파라미터는 &lt;code&gt;Kind.RETURN&lt;/code&gt; 위치에서만 쓸 수 있습니다. 값의 단위는 나노초라서 1000으로 나눠 마이크로초로 기록했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Aggregation&lt;/code&gt; : 호출마다 값을 출력하면 부하도 크고 읽기도 어려우므로, &lt;code&gt;COUNT`와 `AVERAGE&lt;/code&gt; 집계 함수로 요약값만 유지합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@OnEvent&lt;/code&gt; : BTrace 클라이언트에서 이벤트를 보내면 실행되는 핸들러입니다. 여기서 집계 결과를 출력하고 `Sys.exit(0)`으로 세션을 끝냅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저장소에는 `PrintQuery.java`도 있는데, 실행 쿼리 출력용으로 만들려다 만 것인지 `ConnectionMonitor.java`와 내용이 같은 사본입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;2855201.html&quot;&gt;이전 BTrace 글&lt;/a&gt;에서 소개했던 DBCP 모니터링 스크립트들(&lt;code&gt;DbcpMonitor.java&lt;/code&gt;, &lt;code&gt;DbcpMonitorSimple.java&lt;/code&gt;, &lt;code&gt;DbcpActiveConnectionMonitor.java&lt;/code&gt;)도 gist에서 이 저장소로 옮겨 함께 두었습니다. CUBRID 드라이버와 Spring 서블릿을 추적하던 &lt;code&gt;CubridConnectionMonitor.java&lt;/code&gt;, `DaoMonitor.java`도 같은 gist에서 옮긴 스크립트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실행_방법&quot;&gt;실행 방법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BTrace 클라이언트로 대상 JVM의 pid를 지정해서 실행합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;btrace &amp;lt;pid&amp;gt; ConnectionMonitor.java&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;측정을 끝내고 싶을 때 클라이언트에서 Ctrl+C를 누르면 이벤트 전송 여부를 묻는 메뉴가 나옵니다. 이벤트를 보내면 &lt;code&gt;@OnEvent&lt;/code&gt; 핸들러가 실행되어 아래처럼 요약이 출력됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;## com.mysql.jdbc.ConnectionImp.prepareStatement()
- call count :  128
- average duration(microseconds) : 42
## com.mysql.jdbc.ConnectionImpl.setAutoCommit()
- call count :  64
- average duration(microseconds) : 120
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/btrace-scripts/pom.xml&quot;&gt;pom.xml&lt;/a&gt;에 선언한 &lt;code&gt;btrace-client&lt;/code&gt; 의존성은 빌드용이 아니라 IDE에서 BTrace API의 자동완성과 컴파일 검증을 받기 위한 것입니다. 실제 실행은 위처럼 BTrace 클라이언트가 스크립트 파일을 직접 받아서 처리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jdk_25_기준의_선택&quot;&gt;JDK 25 기준의 선택&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 스크립트는 BTrace 1.2와 Connector/J 5.1, JDK 6~8 시절에 작성했습니다. 최신 LTS인 JDK 25 기준으로는 상황이 꽤 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;btrace를_계속_쓴다면&quot;&gt;BTrace를 계속 쓴다면&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BTrace는 java.net(kenai) 시절을 지나 현재 &lt;a href=&quot;https://github.com/btraceio/btrace&quot;&gt;GitHub의 btraceio/btrace&lt;/a&gt;에서 관리됩니다. 다만 마지막 릴리스가 2023년 11월의 v2.2.6으로 활동이 뜸합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2.x 버전부터 패키지가 `com.sun.btrace`에서 `org.openjdk.btrace`로 바뀌어 이 스크립트를 그대로 쓸 수는 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Connector/J도 6.0부터 패키지가 `com.mysql.jdbc`에서 `com.mysql.cj`로 바뀌어 추적 대상 클래스명을 바꿔야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 21부터는 &lt;a href=&quot;https://openjdk.org/jeps/451&quot;&gt;JEP 451&lt;/a&gt;에 따라 실행 중인 JVM에 에이전트를 동적으로 붙일 때 경고가 출력됩니다. JDK 25에서도 아직 허용되지만, 경고 없이 쓰려면 대상 JVM을 &lt;code&gt;-XX:+EnableDynamicAgentLoading&lt;/code&gt; 옵션으로 시작해야 합니다. BTrace처럼 attach에 의존하는 도구 전반에 해당하는 변화입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jdk_25라면_jfr만으로_가능&quot;&gt;JDK 25라면 JFR만으로 가능&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 스크립트가 하던 &quot;특정 메서드의 호출 횟수와 평균 실행 시간 집계&quot;는 JDK 25부터 외부 도구 없이 JFR(Java Flight Recorder)로 할 수 있습니다. &lt;a href=&quot;https://openjdk.org/jeps/520&quot;&gt;JEP 520: JFR Method Timing &amp;amp; Tracing&lt;/a&gt;이 &lt;code&gt;jdk.MethodTiming&lt;/code&gt;, &lt;code&gt;jdk.MethodTrace&lt;/code&gt; 이벤트를 추가했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;java &apos;-XX:StartFlightRecording:jdk.MethodTiming#filter=com.mysql.cj.jdbc.ConnectionImpl::prepareStatement,filename=timing.jfr&apos; -jar app.jar

jfr view method-timing timing.jfr&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;필터는 &lt;code&gt;클래스::메서드&lt;/code&gt; 형식 외에 클래스 단위, 애너테이션 단위(&lt;code&gt;@com.example.Debug&lt;/code&gt;)로도 지정할 수 있고, jcmd로 실행 중인 JVM에 같은 필터로 기록을 시작할 수도 있습니다. 결과는 메서드별 호출 횟수와 평균 시간 표로 출력되어, `ConnectionMonitor.java`의 요약 출력과 같은 정보를 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 JEP 520은 실행 시간만 기록할 뿐 메서드의 파라미터, 리턴값, 필드값은 보여주지 않습니다. &lt;a href=&quot;2855201.html&quot;&gt;이전 글&lt;/a&gt;의 DBCP 스크립트처럼 객체 내부 값을 들여다보는 용도라면 여전히 BTrace나 &lt;a href=&quot;https://github.com/alibaba/arthas&quot;&gt;Arthas&lt;/a&gt;(&lt;code&gt;watch&lt;/code&gt;, &lt;code&gt;monitor&lt;/code&gt;, &lt;code&gt;trace&lt;/code&gt; 명령) 같은 계측 도구가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;그_밖의_대안&quot;&gt;그 밖의 대안&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;특정 메서드가 아닌 전체적인 프로파일링이라면 JFR 상시 기록이나 &lt;a href=&quot;https://github.com/async-profiler/async-profiler&quot;&gt;async-profiler&lt;/a&gt;가 우선 선택지입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL 실행 관찰이 목적이라면 &lt;a href=&quot;https://github.com/jdbc-observations/datasource-proxy&quot;&gt;datasource-proxy&lt;/a&gt;나 &lt;a href=&quot;https://github.com/p6spy/p6spy&quot;&gt;p6spy&lt;/a&gt;처럼 JDBC 계층을 감싸는 라이브러리도 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/btrace-scripts&quot;&gt;예제 저장소 (examples/btrace-scripts)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/btraceio/btrace&quot;&gt;BTrace (btraceio/btrace)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/520&quot;&gt;JEP 520: JFR Method Timing &amp;amp; Tracing&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/451&quot;&gt;JEP 451: Prepare to Disallow the Dynamic Loading of Agents&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;2855201.html&quot;&gt;Btrace로 DBCP의 connection정보를 모니터링 하기&lt;/a&gt; — 이 블로그의 이전 BTrace 글&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Dell XPS 13 웹캠을 살린 커널 모듈 한 줄 패치</title>
      <link>https://blog.benelog.net//dell-xps-webcam-patch.html</link>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">dell-xps-webcam-patch.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#lunar_lake의_카메라_아키텍처&quot;&gt;1. Lunar Lake의 카메라 아키텍처&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#전통적인_노트북_카메라_구조&quot;&gt;1.1. 전통적인 노트북 카메라 구조&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lunar_lake의_connected_cameracvs_모드&quot;&gt;1.2. Lunar Lake의 Connected Camera(CVS) 모드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#이_머신의_구성&quot;&gt;1.3. 이 머신의 구성&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#증상과_실패_체인&quot;&gt;2. 증상과 실패 체인&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#잘못_짚었던_가설&quot;&gt;3. 잘못 짚었던 가설&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#버그가_만들어진_과정&quot;&gt;4. 버그가 만들어진 과정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#패키지_업데이트로는_해결되지_않는_이유&quot;&gt;5. 패키지 업데이트로는 해결되지 않는 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#해결_방법&quot;&gt;6. 해결 방법&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#한_줄_패치&quot;&gt;6.1. 한 줄 패치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dkms로_패키지_모듈_오버라이드&quot;&gt;6.2. DKMS로 패키지 모듈 오버라이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#권한_설정과_캡처_확인&quot;&gt;6.3. 권한 설정과 캡처 확인&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#왜_전에는_됐다가_안_됐을까&quot;&gt;7. 왜 전에는 됐다가 안 됐을까&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#왜_널리_보고되지_않았을까&quot;&gt;8. 왜 널리 보고되지 않았을까&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#업스트림_반영&quot;&gt;9. 업스트림 반영&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;10. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dell XPS 13 9350(Lunar Lake)에 Ubuntu 24.04를 설치한 이후로 웹캠이 동작하지 않았습니다.
&lt;a href=&quot;installing-ubuntu-24.04.html&quot;&gt;Ubuntu 24.04 설치&lt;/a&gt;와 &lt;a href=&quot;oem-kernel.html&quot;&gt;Ubuntu에서의 OEM 커널 설치&lt;/a&gt; 글에서 언급했던 그 문제입니다.
2026년 3월 6일부터 4월 10일까지 약 한 달간 원인을 추적한 끝에, Intel CVS 드라이버의 한 줄 버그가 원인임을 찾아 DKMS 패치로 해결했습니다.
이후 업스트림에 PR을 보냈고 2026년 5월에 머지되었습니다.
이 글에서는 그 과정에서 알게 된 Lunar Lake의 카메라 아키텍처, 잘못 짚었던 가설, 진짜 원인을 찾은 방법, 그리고 버그가 만들어진 업스트림 커밋 이력을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;lunar_lake의_카메라_아키텍처&quot;&gt;1. Lunar Lake의 카메라 아키텍처&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원인을 이해하려면 먼저 이 플랫폼의 카메라 구조를 알아야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;전통적인_노트북_카메라_구조&quot;&gt;1.1. 전통적인 노트북 카메라 구조&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일반적인 노트북 카메라는 단순한 경로를 따릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;센서(OV02C10) ──MIPI CSI-2──→ ISP(IPU) ──→ V4L2 ──→ 앱
       ↑
  INT3472 (전원 관리: GPIO로 dvdd/avdd 제어)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;INT3472 컨트롤러가 GPIO를 통해 센서에 전원을 공급하고, 센서는 MIPI CSI-2 레인으로 ISP(Image Signal Processor)에 직접 연결됩니다.
드라이버 스택이 성숙해서 대부분의 Linux 배포판에서 잘 동작합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;lunar_lake의_connected_cameracvs_모드&quot;&gt;1.2. Lunar Lake의 Connected Camera(CVS) 모드&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intel Core Ultra 200V(Lunar Lake)는 Computer Vision Sensing(CVS)이라는 새로운 카메라 서브시스템을 도입했습니다.
AI 비전 처리를 위한 저전력 전용 컨트롤러가 추가된 구조입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;센서(OV02C10) ──MIPI CSI-2──→ IPU7 ISP ──→ Camera HAL ──→ 앱
       ↑                          ↑
  CVS 컨트롤러(INTC10DE)          │
       ↑                     소유권 이전
  USBIO 브리지(INTC10B5)    (GPIO 핸드셰이크)
       ↑
    USB I2C&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CVS 모드에서는 세 가지가 달라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;전원 경로가 다릅니다. 센서는 INT3472가 아닌 CVS 컨트롤러 경로로 전원을 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;소유권 협상이 필요합니다. CVS 컨트롤러가 먼저 센서를 초기화하고, I2C 프로토콜 협상을 마친 뒤, GPIO 핸드셰이크로 IPU7에 센서 소유권을 넘깁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로토콜 버전 협상이 있습니다. CVS 컨트롤러와 센서 펌웨어 간에 매직 넘버(&lt;code&gt;0xCAFEB0BA&lt;/code&gt;)로 프로토콜 버전(1.0 vs 2.0+)을 식별합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;이_머신의_구성&quot;&gt;1.3. 이 머신의 구성&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ACPI 테이블을 직접 읽어서 확인한 이 머신의 카메라 구성은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;ACPI 속성&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;값&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;LCHS&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;1&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Connected Camera 모드 활성&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;L1CL&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;0xFF&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OVTI02C1은 INT3472에 의존하지 않음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;LNK1._DEP&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;{CVSS, VIC1}&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;CVS 컨트롤러 + USB I2C 브리지에 의존&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 설정에서 OVTI02C1(OV02C10 센서)은 전원과 초기화를 전적으로 CVS 경로에 의존합니다.
CVS probe가 실패하면 센서에 전원이 공급되지 않고, 그 뒤의 모든 단계가 멈춥니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;증상과_실패_체인&quot;&gt;2. 증상과 실패 체인&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;웹캠이 동작하지 않는 상태에서 &lt;code&gt;/dev/video0&lt;/code&gt; 은 v4l2loopback 가상 디바이스일 뿐, 실제 카메라 디바이스는 없었습니다.
dmesg에는 다음의 에러가 남았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Intel CVS driver i2c-INTC10DE:00: magic number in dev response not supported
Intel CVS driver i2c-INTC10DE:00: cvs_find_magic_num_support:Device protocol is 1.0
Intel CVS driver i2c-INTC10DE:00: cvs_common_probe:set_host_identifier cmd failed
Intel CVS driver i2c-INTC10DE:00: probe with driver Intel CVS driver failed with error -5
ov02c10 i2c-OVTI02C1:00: failed to find sensor: -6&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실패의 연쇄를 따라가면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;CVS probe가 시작되고, &lt;code&gt;cvs_find_magic_num_support()&lt;/code&gt; 함수가 이 장치의 펌웨어를 &quot;프로토콜 1.0&quot;으로 판정합니다(&lt;code&gt;magic_num_support = false&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그런데도 드라이버는 &lt;code&gt;cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0)&lt;/code&gt; 명령을 무조건 호출합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로토콜 1.0 펌웨어는 이 명령을 이해하지 못해서 &lt;code&gt;-EIO&lt;/code&gt;를 반환하고, CVS probe가 실패합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;센서 소유권 이전이 일어나지 않아 센서에 전원이 공급되지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ov02c10 드라이버가 I2C로 센서 접근을 시도하지만 전원이 없어 &lt;code&gt;-ENXIO&lt;/code&gt;로 실패하고, 웹캠을 쓸 수 없게 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;근본 원인은 한 줄입니다. &lt;strong&gt;프로토콜 1.0 장치에 프로토콜 2.0+ 전용 명령(&lt;code&gt;SET_HOST_IDENTIFIER&lt;/code&gt;)을 무조건 보내는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;잘못_짚었던_가설&quot;&gt;3. 잘못 짚었던 가설&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음부터 이 원인을 찾은 것은 아닙니다.
dmesg에는 CVS 에러와 함께 &lt;code&gt;int3472-discrete: GPIO type 0x02 unknown&lt;/code&gt; 이라는 에러도 있었습니다.
처음에는 이 INT3472 GPIO 에러를 원인으로 보고, &lt;code&gt;discrete.c&lt;/code&gt; 에 &apos;OVTI02C1 + 0x02 → DOVDD&apos; 매핑을 추가하는 DKMS 패치를 작성했습니다.
빌드와 설치까지 했으나 재부팅 후에도 증상이 그대로였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이후 ACPI NVS 메모리를 직접 읽고 나서야 두 가지를 알게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;GPIO type 0x02 unknown&lt;/code&gt; 에러는 이 노트북에 함께 달려 있는 다른 센서(HIMX1092)에 대한 것이었고, OVTI02C1과는 무관했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LCHS=1&lt;/code&gt; (Connected Camera 모드)에서 OVTI02C1은 INT3472가 아닌 Intel CVS 경로로 전원을 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;에러 메시지가 여러 개 보일 때, 눈에 먼저 띄는 에러가 내 문제의 원인이라는 보장이 없다는 교훈을 얻었습니다.
INT3472 패치는 폐기하고 CVS 드라이버 분석으로 방향을 바꿨습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 외에도 시도했다가 막힌 경로들이 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;시도&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;커널 6.11/6.14로 다운그레이드&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;동일한 원인으로 동일한 증상&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;media-ctl&lt;/code&gt; + &lt;code&gt;v4l2-ctl --stream-mmap&lt;/code&gt; 직접 캡처&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;STREAMON&lt;/code&gt; 실패 (IPU7은 PSys 경유가 필요)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;libcamera&lt;/code&gt; (&lt;code&gt;cam --list&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;IPU7 pipeline handler가 아직 없음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;버그가_만들어진_과정&quot;&gt;4. 버그가 만들어진 과정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intel이 공개한 &lt;a href=&quot;https://github.com/intel/vision-drivers&quot;&gt;intel/vision-drivers&lt;/a&gt; 저장소의 커밋 이력을 추적하면 버그가 만들어진 과정이 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3335%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;날짜&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;커밋&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;내용&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;SET_HOST_IDENTIFIER 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2024-10-22&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/intel/vision-drivers/commit/93244d7&quot;&gt;&lt;code&gt;93244d7&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SET_HOST_IDENTIFIER 명령 최초 추가&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;if (!i2c_shared)&lt;/code&gt; 가드 (부분적 보호)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2024-11-26&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/intel/vision-drivers/commit/23f3733&quot;&gt;&lt;code&gt;23f3733&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;magic_num_support&lt;/code&gt; 필드 도입, 매직 넘버 검증 추가&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;변경 없음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2025-03-12&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/intel/vision-drivers/commit/69d20a1&quot;&gt;&lt;code&gt;69d20a1&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;코드 정리 중 &lt;code&gt;i2c_shared&lt;/code&gt; 가드 제거&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;무조건 호출로 변경&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2025-06-04&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/intel/vision-drivers/commit/14d0181&quot;&gt;&lt;code&gt;14d0181&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;Fix I2C read for protocol 1.0 firmware&quot;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cvs_get_device_cap&lt;/code&gt; 만 가드, SET_HOST_IDENTIFIER 누락&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버그는 두 단계에 걸쳐 만들어졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 번째 단계는 &lt;a href=&quot;https://github.com/intel/vision-drivers/commit/69d20a1&quot;&gt;&lt;code&gt;69d20a1&lt;/code&gt;&lt;/a&gt; (2025-03-12)입니다.
&quot;Remove LJCA WA | Code Cleanup&quot;이라는 커밋에서 코드 정리를 하면서 &lt;code&gt;i2c_shared&lt;/code&gt; 조건 분기를 제거했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;diff&quot;&gt;-  if (!icvs-&amp;gt;i2c_shared) {
-      ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);
-      ...
-  }
+  ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원래 &lt;code&gt;i2c_shared&lt;/code&gt;는 완벽한 가드는 아니었지만, 일부 장치에서 SET_HOST_IDENTIFIER 호출을 건너뛸 수 있게 해줬습니다.
이 제거로 SET_HOST_IDENTIFIER가 모든 장치에서 무조건 실행되게 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 번째 단계는 &lt;a href=&quot;https://github.com/intel/vision-drivers/commit/14d0181&quot;&gt;&lt;code&gt;14d0181&lt;/code&gt;&lt;/a&gt; (2025-06-04)입니다.
&quot;Fix I2C read for protocol 1.0 firmware&quot;라는 제목으로 프로토콜 1.0 지원을 추가한 커밋입니다.
&lt;code&gt;cvs_find_magic_num_support()&lt;/code&gt; 함수를 만들어 프로토콜 버전을 감지하고, &lt;code&gt;cvs_get_device_cap()&lt;/code&gt; 호출은 &lt;code&gt;magic_num_support&lt;/code&gt; 조건으로 감쌌습니다.
하지만 바로 아래의 &lt;code&gt;SET_HOST_IDENTIFIER&lt;/code&gt; 호출은 같은 조건으로 감싸는 것을 빠뜨렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;diff&quot;&gt;+  ret = cvs_find_magic_num_support(icvs);
+  if (ret)
+      goto exit;
+
+  if (icvs-&amp;gt;magic_num_support) {
+      ret = cvs_get_device_cap(&amp;amp;icvs-&amp;gt;cv_fw_capability);
+      if (ret)
+          goto exit;
+  }
+
   ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);  // ← 가드 없음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 함수 안에서, 같은 패턴의 가드가 필요한 두 호출 중 하나만 수정한 전형적인 누락입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;패키지_업데이트로는_해결되지_않는_이유&quot;&gt;5. 패키지 업데이트로는 해결되지 않는 이유&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ubuntu의 &lt;code&gt;linux-modules-vision-*-oem&lt;/code&gt; 패키지는 Intel 업스트림의 특정 시점 스냅샷을 빌드한 것입니다.
당시 설치되어 있던 &lt;code&gt;6.17.0-1017.17&lt;/code&gt; 패키지는 업스트림 HEAD를 기반으로 했지만, 업스트림 자체에 버그가 있으므로 패키지를 아무리 업데이트해도 수정되지 않는 상황이었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;패키지 모듈에 수정이 포함되었는지는 바이너리 역어셈블(&lt;code&gt;objdump&lt;/code&gt;)로 확인했습니다.
Ubuntu 패키지의 &lt;code&gt;intel_cvs.ko&lt;/code&gt;와 패치를 적용한 DKMS 빌드의 &lt;code&gt;cvs_common_probe&lt;/code&gt; 함수를 비교하면, &lt;code&gt;SET_HOST_IDENTIFIER&lt;/code&gt; 호출(&lt;code&gt;mov edi, 0x805&lt;/code&gt;) 직전에 &lt;code&gt;magic_num_support&lt;/code&gt; 검사와 조건 점프가 있는지가 다릅니다.
이 확인 기법은 &lt;a href=&quot;linux-binary-disassembly.html&quot;&gt;Linux 바이너리 역어셈블로 커널 모듈 패치 확인하기&lt;/a&gt; 글에서 따로 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;해결_방법&quot;&gt;6. 해결 방법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;한_줄_패치&quot;&gt;6.1. 한 줄 패치&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;수정 자체는 한 줄의 &lt;code&gt;if&lt;/code&gt; 문 추가입니다.
기존 &lt;code&gt;cvs_get_device_cap()&lt;/code&gt; 가드와 완전히 동일한 패턴입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;diff&quot;&gt;-		ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);
-		if (ret) {
-			dev_err(cvs-&amp;gt;dev, &quot;%s:set_host_identifier cmd failed&quot;, __func__);
-			goto exit;
+		if (icvs-&amp;gt;magic_num_support) {
+			ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);
+			if (ret) {
+				dev_err(cvs-&amp;gt;dev, &quot;%s:set_host_identifier cmd failed&quot;, __func__);
+				goto exit;
+			}
 		}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;dkms로_패키지_모듈_오버라이드&quot;&gt;6.2. DKMS로 패키지 모듈 오버라이드&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;업스트림 소스에 위의 패치를 적용하고 DKMS(&lt;code&gt;vision-driver/1.0.0&lt;/code&gt;)로 빌드해서 설치했습니다.
DKMS 모듈은 &lt;code&gt;/lib/modules/&amp;lt;커널 버전&amp;gt;/updates/dkms/&lt;/code&gt; 경로에 설치되는데, 이 경로가 패키지 모듈 경로(&lt;code&gt;ubuntu/vision/&lt;/code&gt;)보다 우선 로드됩니다.
패키지를 건드리지 않고 모듈만 바꿔치기할 수 있고, 커널이 업데이트되어도 DKMS가 자동으로 다시 빌드해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;install-vision-dkms.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/bin/bash
set -e

SRC=&quot;$HOME/work/vision-drivers&quot;  # 패치 적용한 소스 위치
DEST=&quot;/usr/src/vision-driver-1.0.0&quot;

sudo cp -r &quot;$SRC&quot; &quot;$DEST&quot;
sudo rm -rf &quot;$DEST/.git&quot;

sudo dkms add vision-driver/1.0.0
sudo dkms build vision-driver/1.0.0
sudo dkms install vision-driver/1.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;재부팅 후 실제 로드된 모듈이 DKMS 빌드인지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;modinfo intel_cvs | grep filename
# OK:   /lib/modules/.../updates/dkms/intel_cvs.ko.zst
# FAIL: /lib/modules/.../ubuntu/vision/intel_cvs.ko.zst (패키지 원본)

journalctl -k -b 0 --no-pager | grep -E &quot;cvs|CVS|INTC10DE&quot; | head -10
# &quot;Transfer of ownership success&quot; 확인&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;권한_설정과_캡처_확인&quot;&gt;6.3. 권한 설정과 캡처 확인&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;패치만으로는 부족하고 권한 설정도 필요했습니다.
사용자를 video 그룹에 추가하고, IPU7 PSys 장치의 권한을 udev 규칙으로 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo usermod -aG video $USER

echo &apos;SUBSYSTEM==&quot;intel-ipu7-psys&quot;, MODE=&quot;0660&quot;, GROUP=&quot;video&quot;&apos; \
  | sudo tee /etc/udev/rules.d/50-ipu7.rules&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IPU7은 아직 libcamera pipeline handler가 없어서 &lt;code&gt;cam --list&lt;/code&gt; 에 표시되지 않습니다.
Intel Camera HAL(&lt;code&gt;libcamhal-ipu7x&lt;/code&gt;)과 GStreamer의 &lt;code&gt;icamerasrc&lt;/code&gt; 플러그인으로 캡처를 확인했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gst-launch-1.0 icamerasrc device-name=ov02c10-uf num-buffers=3 \
  ! &quot;video/x-raw,format=NV12&quot; ! jpegenc ! filesink location=/tmp/test.jpg&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 세 가지를 모두 적용한 뒤에 1920x1080 캡처에 성공했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;왜_전에는_됐다가_안_됐을까&quot;&gt;7. 왜 전에는 됐다가 안 됐을까&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dell이 이 모델을 출시할 때는 당연히 카메라가 동작하는 상태로 테스트했을 것입니다.
무엇이 바뀌었을까요?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 유력한 추정은 Ubuntu OEM 커널의 vision-drivers 스냅샷 시점입니다.
Ubuntu OEM 커널 패키지가 빌드되는 시점에 따라 포함되는 vision-drivers 버전이 달라집니다.
Dell 출하 시점의 Ubuntu는 SET_HOST_IDENTIFIER가 추가되기 전이거나 &lt;code&gt;i2c_shared&lt;/code&gt; 가드가 있던 버전의 vision-drivers를 사용했을 가능성이 높습니다.
이후 OEM 커널 업데이트로 새 스냅샷이 적용되면서, &lt;code&gt;i2c_shared&lt;/code&gt; 가드가 제거된(&lt;a href=&quot;https://github.com/intel/vision-drivers/commit/69d20a1&quot;&gt;&lt;code&gt;69d20a1&lt;/code&gt;&lt;/a&gt; 이후) 버전이 들어온 것으로 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;센서 펌웨어 업데이트나 모듈 로드 순서 변경 같은 다른 가능성도 검토했지만, 펌웨어를 업데이트한 이력이 없고 에러의 성격도 달라서 가능성이 낮다고 판단했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;왜_널리_보고되지_않았을까&quot;&gt;8. 왜 널리 보고되지 않았을까&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 버그가 광범위하게 보고되지 않은 이유도 짐작해볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;Lunar Lake 자체가 새로운 플랫폼입니다. 2024년 하반기에 출시되어 Linux 사용자 기반이 아직 작습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CVS 모드는 일부 구성에서만 활성화됩니다. 같은 Lunar Lake라도 &lt;code&gt;LCHS=0&lt;/code&gt; (전통 모드)이면 CVS 드라이버를 거치지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로토콜 1.0이 구형입니다. 최신 CVS 펌웨어는 프로토콜 2.0+를 지원하므로 SET_HOST_IDENTIFIER가 성공합니다. Dell XPS 13 9350의 OV02C10은 프로토콜 1.0 펌웨어를 사용하는, 상대적으로 드문 조합입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Windows에서는 드라이버 스택이 다릅니다. Intel은 Windows용 드라이버를 별도로 관리하므로, Linux 전용 업스트림 저장소의 버그가 Windows에서는 나타나지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intel 내부 개발팀이 주로 프로토콜 2.0+ 장치로 테스트하기 때문에, 프로토콜 1.0 장치에서만 발생하는 이 문제를 발견하지 못한 것으로 추정됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;업스트림_반영&quot;&gt;9. 업스트림 반영&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로컬 DKMS 패치는 커널 업데이트 때마다 유지보수 부담이 남습니다.
근본적인 해결을 위해 2026년 4월 10일에 &lt;a href=&quot;https://github.com/intel/vision-drivers/pull/35&quot;&gt;intel/vision-drivers#35&lt;/a&gt;로 수정 PR을 제출했습니다.
이후 같은 수정에 인접한 수정 두 건(&lt;code&gt;cvs_init()&lt;/code&gt; 에러 전파, &lt;code&gt;find_oem_prod_id&lt;/code&gt; 에서의 &lt;code&gt;ACPI_HANDLE()&lt;/code&gt; 사용)을 묶은 &lt;a href=&quot;https://github.com/intel/vision-drivers/pull/38&quot;&gt;#38&lt;/a&gt;로 정리해서 다시 올렸고, 2026년 5월 7일에 머지되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;업스트림에 머지된 수정이 Ubuntu의 vision-drivers 스냅샷 갱신을 거쳐 OEM 커널 패키지로 릴리스되면 DKMS 모듈은 제거할 수 있습니다.
패키지 모듈에 수정이 포함되었는지는 앞서 소개한 역어셈블 방법이나 &lt;code&gt;strings&lt;/code&gt; 명령으로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;10. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/intel/vision-drivers&quot;&gt;intel/vision-drivers&lt;/a&gt; — Intel CVS 드라이버 업스트림 저장소&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/intel/vision-drivers/pull/38&quot;&gt;intel/vision-drivers#38&lt;/a&gt; — 이 문제를 수정한 PR&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.ubuntu.com/IntelMIPICamera&quot;&gt;Intel MIPI Camera — Ubuntu Wiki&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;linux-binary-disassembly.html&quot;&gt;Linux 바이너리 역어셈블로 커널 모듈 패치 확인하기&lt;/a&gt; — 패키지 모듈과 DKMS 모듈의 바이너리 비교 기법&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;oem-kernel.html&quot;&gt;Ubuntu에서의 OEM 커널 설치&lt;/a&gt; — OEM 커널과 HWE 커널의 차이&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>junit-benchmarks로 HTTP API의 응답 시간 측정하기</title>
      <link>https://blog.benelog.net//junit-benchmarks.html</link>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">junit-benchmarks.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#junit_benchmarks_소개&quot;&gt;junit-benchmarks 소개&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#예제_구조&quot;&gt;예제 구조&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#실행_방법&quot;&gt;실행 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#지금_다시_쓴다면&quot;&gt;지금 다시 쓴다면&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예전 예제 저장소들을 정리하면서 &lt;code&gt;benelog/benchmark&lt;/code&gt; 저장소를 이 블로그 저장소의 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/benchmark-test&quot;&gt;examples/benchmark-test&lt;/a&gt;로 옮겼습니다. 원래 저장소에 README가 없어서 어떤 코드인지 이 글로 설명을 남깁니다. 이 예제는 JUnit 4 확장 라이브러리인 junit-benchmarks로 HTTP API에 동시 요청을 보내 응답 시간을 측정합니다. 2012년경 사내 API 서버가 동시 요청을 받았을 때의 응답 시간을 확인하려고 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;junit_benchmarks_소개&quot;&gt;junit-benchmarks 소개&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/carrotsearch/junit-benchmarks&quot;&gt;junit-benchmarks&lt;/a&gt;는 Carrot Search에서 만든 JUnit 4 확장입니다. 평범한 JUnit 테스트 메서드에 애너테이션만 붙여서 벤치마크로 바꿔줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@BenchmarkOptions&lt;/code&gt; : 반복 횟수(&lt;code&gt;benchmarkRounds&lt;/code&gt;), 워밍업 횟수(&lt;code&gt;warmupRounds&lt;/code&gt;), 동시 실행 스레드 수(&lt;code&gt;concurrency&lt;/code&gt;)를 지정합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@BenchmarkMethodChart&lt;/code&gt; : 메서드별 실행 시간을 HTML 차트로 생성합니다. 결과를 H2 DB에 누적해서 차트를 그리는 방식이라, 예제의 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/benchmark-test/pom.xml&quot;&gt;pom.xml&lt;/a&gt;에 &lt;code&gt;h2&lt;/code&gt; 의존성이 들어 있는 이유이기도 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전용 부하 테스트 도구를 준비할 정도는 아니고, 개발 중에 IDE나 `mvn test`로 바로 돌려볼 간단한 측정이 필요할 때 편했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;예제_구조&quot;&gt;예제 구조&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/benchmark-test/src/test/java/net/benelog/test/performance/SampleBenchmarkTest.java&quot;&gt;SampleBenchmarkTest.java&lt;/a&gt;가 핵심입니다. 핵심 부분만 옮기면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SampleBenchmarkTest.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@BenchmarkMethodChart(filePrefix = &quot;benchmark-sample&quot;)
public class SampleBenchmarkTest extends AbstractBenchmark {

	@Test
	@BenchmarkOptions(benchmarkRounds = 8, warmupRounds = 0, concurrency = 8)
	public void testCreateApk() throws ClientProtocolException, IOException, InterruptedException {
		String requestUrl = &quot;http://localhost/create&quot;;
		HttpPost request = new HttpPost(requestUrl);
		request.setEntity(createEntity());

		HttpResponse response = httpClient.execute(request);

		int statusCode = response.getStatusLine().getStatusCode();
		assertThat(statusCode, is(200));
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구성 요소별로 보면 이렇습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AbstractBenchmark&lt;/code&gt; 상속 : junit-benchmarks의 측정 Rule이 적용된 기반 클래스입니다. 이 클래스를 상속하면 &lt;code&gt;@Test&lt;/code&gt; 메서드가 벤치마크 대상이 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@BenchmarkOptions(benchmarkRounds = 8, warmupRounds = 0, concurrency = 8)&lt;/code&gt; : 총 8회 호출을 8개 스레드로 나눠 실행합니다. 즉 동시에 8건의 요청이 서버로 나갑니다. 측정 대상이 JVM 내부 코드가 아니라 외부 HTTP 서버라서 워밍업은 0으로 두었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;요청 내용 : 로컬 서버의 APK 생성 API(&lt;code&gt;/create&lt;/code&gt;)에 &lt;code&gt;versionCode&lt;/code&gt; 파라미터를 담아 POST를 보내고, 응답 코드가 200인지 확인합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HTTP 호출에는 Apache HttpClient 4.1을 사용했습니다. 동시 요청 8건이 커넥션을 기다리며 막히지 않도록, `ThreadSafeClientConnManager`로 커넥션 풀의 최대치를 스레드 수보다 큰 9로 잡았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;SampleBenchmarkTest.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;private static void createHttpClient(int connectTimeoutMilsec, int readTimeoutMilsec) {
	connManager = new ThreadSafeClientConnManager();
	connManager.setMaxTotal(9);
	connManager.setDefaultMaxPerRoute(9);

	HttpClient client = new DefaultHttpClient(connManager);
	client.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, connectTimeoutMilsec);
	client.getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, readTimeoutMilsec);
	httpClient = client;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같이 들어 있는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/benchmark-test/src/test/java/net/benelog/test/performance/SimpleRequestTest.java&quot;&gt;SimpleRequestTest.java&lt;/a&gt;는 벤치마크 없이 GET 요청 한 번을 보내는 단순 테스트입니다. 다만 지금 보면 &lt;code&gt;BasicHttpParams`에 `query&lt;/code&gt; 값을 넣어 `request.setParams()`로 전달하는 부분은 의도대로 동작하지 않는 코드입니다. `HttpParams`는 타임아웃 같은 클라이언트 실행 옵션을 담는 자리라서, 이렇게 넣은 값은 URL 쿼리 스트링으로 전송되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실행_방법&quot;&gt;실행 방법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`mvn test`나 IDE에서 일반 JUnit 테스트처럼 실행하면 콘솔에 라운드별 실행 시간의 평균과 표준편차가 아래 형태로 출력됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;SampleBenchmarkTest.testCreateApk: [measured 8 out of 8 rounds, threads: 8 (concurrent)]
 round: 0.52 [+- 0.06], round.block: 0.01 [+- 0.02], round.gc: 0.00 [+- 0.00],
 GC.calls: 0, GC.time: 0.00, time.total: 0.61, time.warmup: 0.00, time.bench: 0.61&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`@BenchmarkMethodChart`의 HTML 차트까지 받으려면 결과를 H2 DB에 쌓도록 시스템 프로퍼티를 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;mvn test -Djub.consumers=CONSOLE,H2 -Djub.db.file=.benchmarks&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;지금_다시_쓴다면&quot;&gt;지금 다시 쓴다면&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 예제는 JUnit 4.8, HttpClient 4.1, junit-benchmarks 0.3 시절에 작성했습니다. 지금 기준으로는 구성 요소 전부가 세대교체되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;junit-benchmarks는 2013년의 0.7.2 릴리스를 끝으로 개발이 중단되었고, 제작사도 JVM 마이크로벤치마크 용도로는 &lt;a href=&quot;https://github.com/openjdk/jmh&quot;&gt;JMH&lt;/a&gt;를 권합니다. 다만 JMH는 JVM 내부 코드 측정에 특화된 도구라, 이 예제처럼 외부 HTTP 서버에 부하를 거는 용도와는 초점이 다릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTTP 부하 테스트가 목적이라면 지금은 &lt;a href=&quot;https://k6.io/&quot;&gt;k6&lt;/a&gt;, &lt;a href=&quot;https://gatling.io/&quot;&gt;Gatling&lt;/a&gt;, &lt;a href=&quot;https://jmeter.apache.org/&quot;&gt;JMeter&lt;/a&gt; 같은 전용 도구가 우선 선택지입니다. 이 예제 수준의 간단한 측정이라면 &lt;a href=&quot;https://github.com/rakyll/hey&quot;&gt;hey&lt;/a&gt;나 wrk 같은 CLI 도구가 코드 작성 없이 더 빠릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HttpClient 4.1의 `DefaultHttpClient`와 `ThreadSafeClientConnManager`는 4.3에서 deprecated 되었습니다. Java에서 직접 호출 코드를 짠다면 HttpClient 5.x의 `PoolingHttpClientConnectionManager`나, JDK 11부터 표준으로 들어온 `java.net.http.HttpClient`를 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;동시 요청 시에도 응답이 정상인지&quot;를 회귀 테스트로 유지하고 싶다면, 응답 시간 측정과 기능 검증을 분리해서 후자만 CI에 두는 편이 안정적입니다. 응답 시간은 실행 환경에 따라 흔들려서 테스트 통과 기준으로 삼으면 거짓 실패가 잦습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/benchmark-test&quot;&gt;예제 코드 (examples/benchmark-test)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/carrotsearch/junit-benchmarks&quot;&gt;junit-benchmarks (carrotsearch/junit-benchmarks)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jmh&quot;&gt;JMH (Java Microbenchmark Harness)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://hc.apache.org/&quot;&gt;Apache HttpComponents&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring Retry로 구현하는 재시도와 서킷 브레이커</title>
      <link>https://blog.benelog.net//spring-retry.html</link>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">spring-retry.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#실습_프로젝트_초기_설정&quot;&gt;1. 실습 프로젝트 초기 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#retrytemplate과_정책_클래스_활용&quot;&gt;2. RetryTemplate과 정책 클래스 활용&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#retrytemplate과_retrypolicy_backoffpolicy&quot;&gt;2.1. RetryTemplate과 RetryPolicy, BackOffPolicy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#recover_동작_정의&quot;&gt;2.2. Recover 동작 정의&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#circuitbreakerretrypolicy&quot;&gt;2.3. CircuitBreakerRetryPolicy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#애너테이션과_aop를_이용한_재시도&quot;&gt;3. 애너테이션과 AOP를 이용한 재시도&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#retryable&quot;&gt;3.1. @Retryable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#recover&quot;&gt;3.2. @Recover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#circuitbreaker&quot;&gt;3.3. @CircuitBreaker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#retrylistener&quot;&gt;4. RetryListener&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;5. 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;6. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원격 호출처럼 일시적으로 실패할 수 있는 작업에는 재시도가 필요합니다.
&lt;a href=&quot;https://github.com/spring-projects/spring-retry&quot;&gt;Spring Retry&lt;/a&gt;는 재시도 정책과 실행 구조를 제공하는 모듈입니다.
이 글에서는 &lt;code&gt;RetryTemplate&lt;/code&gt; 을 직접 호출하는 방법과 애너테이션 기반의 AOP를 이용하는 방법으로 재시도, 리커버, 서킷 브레이커 정책을 적용해보고, 그 동작을 테스트 코드로 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글은 Spring Retry가 Spring Framework에 흡수되기 전, 독립된 모듈이던 시절의 사용법을 정리한 글입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Batch에 내장되어 있던 재시도 기능은 Spring Batch 2.2.0부터 Spring Retry라는 독립된 모듈로 분리되었습니다.
그 뒤로는 Spring Integration이나 Spring Cloud Netflix 같은 다른 프로젝트에서도 이 모듈을 의존했습니다.
Spring Retry의 기능은 Spring Framework 7.0부터 프레임워크 안으로 흡수되었습니다.
재시도의 핵심이 되는 클래스들은 &apos;spring-core&apos; 모듈에 들어갔습니다.
&lt;code&gt;@Retryable&lt;/code&gt; 애너테이션과 이를 처리하는 AOP 기능은 &apos;spring-context&apos; 모듈에 들어갔습니다.
Spring Batch 6.0, Spring Boot 4.0 이상에서는 &apos;spring-core&apos; 모듈에 포함된 재시도 기능을 활용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최신 버전에서의 유효한 사용법은 &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/resilience.html&quot;&gt;Resilience Features&lt;/a&gt; 문서를 참고할 수 있습니다.
이 글에 소개된 &lt;code&gt;@Recover&lt;/code&gt;와 &lt;code&gt;@CircuitBreaker&lt;/code&gt;는 Spring Framework 7.0의 재시도 기능에서는 지원되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실습_프로젝트_초기_설정&quot;&gt;1. 실습 프로젝트 초기 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 실습에서는 Spring Retry의 기능들을 테스트 코드를 통해 확인해보겠습니다.
IDE에서 새로운 Gradle 프로젝트를 생성합니다.
프로젝트 이름은 &apos;retry&apos; 등으로 편하게 붙입니다.
필요한 의존성 선언 등을 담은 &lt;code&gt;build.gradle&lt;/code&gt; 파일은 다음과 같이 작성합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;plugins {
    id &apos;java&apos;
    id &apos;org.springframework.boot&apos; version &apos;3.2.1&apos;
    id &apos;io.spring.dependency-management&apos; version &apos;1.1.4&apos;
}

group = &apos;net.benelog.retry&apos;
version = &apos;1.0-SNAPSHOT&apos;
repositories {
    mavenCentral()
}

java {
    sourceCompatibility = &apos;21&apos;
}

dependencies {
    implementation &apos;org.springframework.retry:spring-retry&apos; // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
    implementation &apos;ch.qos.logback:logback-classic&apos;
    implementation &apos;org.springframework:spring-core&apos;
    testImplementation &apos;org.springframework.boot:spring-boot-starter-test&apos;
}

test {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;이 실습의 핵심인 spring-retry 모듈 의존성입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글의 예제에서 스프링 부트의 기능을 활용한 요소는 없습니다.
그럼에도 라이브러리 버전을 편리하게 지정하기 위해 Gradle의 스프링 부트 플러그인과 &lt;code&gt;spring-boot-starter-test&lt;/code&gt; 모듈을 추가했습니다.
테스트에서도 꼭 필요한 의존성만을 명시하고 싶다면 JUnit5와 AssertJ를 직접 추가해도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 정의한 &lt;code&gt;NotificationService&lt;/code&gt; 인터페이스를 재시도 대상으로 삼겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationService.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public interface NotificationService {
  boolean send(String message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;src/main/&apos; 폴더 하위에 &lt;code&gt;NotificationService.java&lt;/code&gt; 파일을 추가합니다.
패키지는 &lt;code&gt;net.benelog.retry&lt;/code&gt; 등 편리한 대로 지정합니다.
실제 알림을 보내는 기능은 실습 대상이 아닙니다.
이 인터페이스에 맞추어 알림을 보내기 위해 원격 호출을 하는 구현 클래스가 있다고 상상만 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실제로 원격 호출을 하지 않고 의도적으로 지정한 횟수만큼 실패하는 클래스를 &lt;code&gt;UnstableNotificationService&lt;/code&gt;라는 이름으로 만들겠습니다.
테스트 용도의 클래스이므로 &apos;src/test/&apos; 폴더 하위에 패키지를 만들어서 추가합니다.
&lt;code&gt;send(String)&lt;/code&gt; 메서드를 호출했을 때 지정된 횟수만큼은 예외를 내도록 구현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;UnstableNotificationService.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class UnstableNotificationService implements NotificationService {
	private final Logger logger = LoggerFactory.getLogger(UnstableNotificationService.class);
	private final int failures;
	private int tryCount = 0;

	public UnstableNotificationService(int failures) { // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		this.failures = failures;
	}

	@Override
	public boolean send(String message) {
		this.tryCount++;
		if (this.tryCount &amp;lt;= this.failures) { // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
			throw new RuntimeException(&quot;실패 : &quot; + tryCount);
		}
		logger.info(&quot;성공 : {}, {}&quot;, this.tryCount, message);
		return true;
	}

	public int getTryCount() {
		return this.tryCount; // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;의도적으로 실패할 횟수를 생성자 파라미터로 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;의도한 횟수보다 작거나 같으면 예외를 발생시킵니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;send(String)&lt;/code&gt; 메서드가 실제로 몇 번 호출되었는지 알려줍니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;send(String)&lt;/code&gt; 메서드 호출을 재시도하는 코드를 이어지는 단락에서 작성하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;retrytemplate과_정책_클래스_활용&quot;&gt;2. RetryTemplate과 정책 클래스 활용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;retrytemplate과_retrypolicy_backoffpolicy&quot;&gt;2.1. RetryTemplate과 RetryPolicy, BackOffPolicy&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryTemplate&lt;/code&gt; 은 Spring Retry의 핵심 기능을 구현한 클래스입니다.
이 클래스는 4개의 &lt;code&gt;execute()&lt;/code&gt; 메서드를 제공하는데, 모두 &lt;code&gt;RetryCallback&lt;/code&gt; 인터페이스 타입을 파라미터로 받습니다.
이 메서드들은 &lt;code&gt;RetryOperations&lt;/code&gt; 라는 인터페이스로도 따로 정의되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RetryTemplate.execute() 메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;  public final &amp;lt;T, E extends Throwable&amp;gt; T execute(RetryCallback&amp;lt;T, E&amp;gt; retryCallback) throws E {
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryCallback&lt;/code&gt;은 아래와 같이 하나의 메서드만 정의된 인터페이스로, 재시도 대상이 되는 작업을 구현하는 용도입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RetryCallback 인터페이스&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public interface RetryCallback&amp;lt;T, E extends Throwable&amp;gt; {
  T doWithRetry(RetryContext context) throws E;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞 단락의 &lt;code&gt;NotificationService.send(String)&lt;/code&gt; 메서드를 재시도하는 클래스를 &lt;code&gt;NotificationRetryDecorator&lt;/code&gt; 라는 이름으로 만들어보겠습니다.
디자인 패턴 중 Decorator 패턴을 응용한 클래스임을 이름으로 표현했습니다.
Decorator는 기존 객체를 감싸는 방식으로 유연하게 책임을 추가하는 패턴입니다.
&lt;code&gt;RetryTemplate&lt;/code&gt;이 꼭 Decorator 패턴을 통해 적용되어야 하는 것은 아닙니다.
&lt;code&gt;RetryCallback&lt;/code&gt; 안에서 호출하는 재시도 대상은 내부 &lt;code&gt;private&lt;/code&gt; 메서드나 직접 기술한 긴 로직 등 무엇이든 될 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구현하려는 재시도 요구사항은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;최대 시도 횟수는 &lt;code&gt;NotificationRetryDecorator&lt;/code&gt; 클래스의 생성자 파라미터로 전달받는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도 사이에는 200ms에서 시작해 2배씩 늘어나는 시간 간격을 둔다. 600ms를 최대 간격으로 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;src/main/&apos; 디렉터리 아래 &lt;code&gt;NotificationService&lt;/code&gt; 인터페이스와 같은 패키지에 &lt;code&gt;NotificationRetryDecorator.java&lt;/code&gt; 파일을 추가합니다.
요구사항대로 &lt;code&gt;RetryTemplate&lt;/code&gt;을 생성해주는 private 메서드부터 먼저 만들겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecorator.java에서 RetryTemplate을 생성하는 메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.util.Map;

import org.springframework.retry.RetryOperations;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

public class NotificationRetryDecorator {
	private RetryOperations buildRetryOperations(int maxAttempts) { // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		var retryPolicy = new SimpleRetryPolicy( // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
			maxAttempts,
			Map.of(RuntimeException.class, true)
		);
		var backOffPolicy = new ExponentialBackOffPolicy(); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
		backOffPolicy.setInitialInterval(200L);
		backOffPolicy.setMultiplier(2d);
		backOffPolicy.setMaxInterval(600L);

		var retryTemplate = new RetryTemplate(); // &lt;b class=&quot;conum&quot;&gt;(4)&lt;/b&gt;
		retryTemplate.setRetryPolicy(retryPolicy);
		retryTemplate.setBackOffPolicy(backOffPolicy);
		return retryTemplate;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryTemplate&lt;/code&gt; 클래스가 구현한 인터페이스인 &lt;code&gt;RetryOperations&lt;/code&gt;를 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RuntimeException&lt;/code&gt;이 발생하면 &apos;maxAttempts&apos; 값으로 지정된 횟수만큼 재시도하도록 정책을 정의합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이어지는 세터 호출을 통해 최초 200ms, 이후 2배씩, 최대 600ms까지만 재시도 간격을 늘리는 정책을 정의합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;앞서 정의한 &lt;code&gt;RetryPolicy&lt;/code&gt;, &lt;code&gt;BackOffPolicy&lt;/code&gt;를 참조할 &lt;code&gt;RetryTemplate&lt;/code&gt;의 인스턴스를 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반환형으로 &lt;code&gt;RetryOperations&lt;/code&gt; 인터페이스를 활용했기에 &lt;code&gt;buildRetryOperations(int)&lt;/code&gt; 메서드를 호출해서 사용하는 쪽에서는 &lt;code&gt;RetryTemplate&lt;/code&gt; 클래스 레벨에서만 정의된 &lt;code&gt;setBackOffPolicy(..)&lt;/code&gt; 같은 초기 설정 메서드를 호출할 수 없게 됩니다.
&lt;code&gt;RetryOperations&lt;/code&gt;만 의존하는 코드에서는 &lt;code&gt;execute(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드만 신경 쓰면 됩니다.
혹시 모를 실수를 막고, IDE 자동 완성의 추천 대상도 줄어든다는 장점이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 클래스에서 &lt;code&gt;RetryTemplate&lt;/code&gt;을 동일한 정책으로 쓴다면 애플리케이션 컨텍스트에 등록하고 재시도가 필요한 클래스에서 주입받는 방식도 가능합니다.
그럴 경우에도 이를 참조해서 쓰는 쪽에서 &lt;code&gt;RetryOperations&lt;/code&gt; 인터페이스에만 의존한다면 여러 클래스에서 공유되는 정책이 수정되지 않도록 방어됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NotificationRetryDecorator&lt;/code&gt; 실습을 이어가겠습니다.
Decorator 패턴을 구현하기 위해 &lt;code&gt;NotificationService&lt;/code&gt; 인터페이스를 이 클래스가 구현합니다.
생성자에서는 재시도 대상 객체와 재시도 횟수를 주입받고, 앞선 예제의 &lt;code&gt;buildRetryOperations(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드를 호출하는 등 재시도에 필요한 구성요소를 만들어 갑니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecorator.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;

public class NotificationRetryDecorator implements NotificationService { // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
	private final Logger logger = LoggerFactory.getLogger(NotificationRetryDecorator.class);
	private final NotificationService target;
	private final RetryOperations retryOperations;

	public NotificationRetryDecorator(NotificationService target, int maxAttempts) { // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		this.target = target;
		this.retryOperations = buildRetryOperations(maxAttempts); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
	}

	@Override
	public boolean send(String message) {
		RetryCallback&amp;lt;Boolean, RuntimeException&amp;gt; retryCallback = (RetryContext context) -&amp;gt; { // &lt;b class=&quot;conum&quot;&gt;(4)&lt;/b&gt;
			logger.info(&quot;{}&quot;, context);
			return target.send(message);
		};
		return this.retryOperations.execute(retryCallback);
	}
    // buildRetryOperations(...) 메서드 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;NotificationService&lt;/code&gt; 인터페이스를 구현함으로써 재시도 기능이 없었던 기존의 사용처에도 바로 적용할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도의 대상 객체와 재시도 횟수를 생성자의 파라미터로 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;생성자를 통해 받은 횟수만큼 재시도하는 &lt;code&gt;RetryOperations&lt;/code&gt; 객체를 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도 대상이 되는 동작을 람다 표현식으로 정의했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 예제에서는 파라미터 타입을 더 명확히 드러내고자 &apos;retryCallback&amp;#8217;을 로컬 변수로 할당하고, 실행 여부를 인지하기 쉽도록 콜백 안에서 로깅을 하는 코드도 넣었습니다.
실무에서는 아래와 같이 람다 표현식을 더 간결하게 쓰는 방법도 고려할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RetryCallback 객체를 더 간결하게 정의&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	@Override
	public boolean send(String message) {
		return this.retryOperations.execute((context) -&amp;gt; target.send(message));
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryTemplate&lt;/code&gt; 클래스는 &lt;code&gt;builder()&lt;/code&gt; 메서드도 제공합니다.
직접 &lt;code&gt;RetryPolicy&lt;/code&gt;, &lt;code&gt;BackOffPolicy&lt;/code&gt;를 지정해서 &lt;code&gt;RetryTemplate&lt;/code&gt;을 생성했던 &lt;code&gt;buildRetryOperations(int)&lt;/code&gt; 메서드는 아래와 같이 대체될 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;builder()를 이용한 정책 지정 간소화&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;		this.retryOperations = RetryTemplate.builder()
			.retryOn(RuntimeException.class)
			.maxAttempts(maxAttempts)
			.exponentialBackoff(200L, 2d, 600L)
			.build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;builder()&lt;/code&gt;를 쓰면서도 &lt;code&gt;.customPolicy(RetryPolicy)&lt;/code&gt;, &lt;code&gt;.customBackOff(BackOffPolicy)&lt;/code&gt; 메서드를 통해 직접 Policy 구현 클래스를 지정할 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트 코드를 통해 재시도가 지정한 정책대로 되었는지 확인해보겠습니다.
앞에서 만든 테스트 전용 클래스인 &lt;code&gt;UnstableNotificationService&lt;/code&gt;로 의도적으로 실패할 횟수를 지정하고, 이 횟수를 넘어선 재시도로 결국에는 성공함을 테스트 코드로 표현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecoratorTest.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class NotificationRetryDecoratorTest {
	@Test
	void successByRetry() {
		var target = new UnstableNotificationService(3); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		var decorator = new NotificationRetryDecorator(target, 4); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;

		boolean success = decorator.send(&quot;hello&quot;);

		assertThat(success).isTrue();
		assertThat(target.getTryCount()).isEqualTo(4); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;최초 3회 호출은 예외를 내는 &lt;code&gt;UnstableNotificationService&lt;/code&gt; 인스턴스를 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;대상 객체를 총 4회까지 재시도하도록 decorator를 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;총 4회까지 &lt;code&gt;UnstableNotificationService.send(String)&lt;/code&gt; 메서드가 호출되었음이 확인됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 테스트 코드를 실행하면 &lt;code&gt;UnstableNotificationService.send(String)&lt;/code&gt; 호출이 3번 실패하고 4번째에 성공하는 과정이 콘솔의 로그로도 확인됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecoratorTest가 실행될 때의 콘솔 로그&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;INFO net.benelog.retry.NotificationRetryDecorator -- [RetryContext: count=0, lastException=null, exhausted=false]
INFO net.benelog.retry.NotificationRetryDecorator -- [RetryContext: count=1, lastException=java.lang.RuntimeException: 실패 : 1, exhausted=false]
INFO net.benelog.retry.NotificationRetryDecorator -- [RetryContext: count=2, lastException=java.lang.RuntimeException: 실패 : 2, exhausted=false]
INFO net.benelog.retry.NotificationRetryDecorator -- [RetryContext: count=3, lastException=java.lang.RuntimeException: 실패 : 3, exhausted=false]
INFO net.benelog.retry.UnstableNotificationService -- 성공 : 4, hello&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로그 메시지에서 &lt;code&gt;RetryContext&lt;/code&gt;가 가진 재시도 횟수(&lt;code&gt;count&lt;/code&gt;), 마지막으로 발생한 예외(&lt;code&gt;lastException&lt;/code&gt;)도 확인됩니다.
각각 &lt;code&gt;RetryContext.getRetryCount()&lt;/code&gt;, &lt;code&gt;RetryContext.getLastThrowable()&lt;/code&gt; 메서드로 참조할 수 있는 값입니다.
필요하다면 &lt;code&gt;RetryCallback.doWithRetry(RetryContext)&lt;/code&gt; 메서드를 구현한 메서드나 람다 표현식에서 이를 활용할 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;recover_동작_정의&quot;&gt;2.2. Recover 동작 정의&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;재시도를 했음에도 마지막 시도까지 실패했을 때 다른 동작을 실행할 수도 있습니다.
&lt;code&gt;RetryTemplate&lt;/code&gt;은 &lt;code&gt;execute(RetryCallback, RecoveryCallback)&lt;/code&gt; 메서드를 통해 이를 지원합니다.
재시도할 동작은 &lt;code&gt;RetryCallback&lt;/code&gt;에, 최종 실패 시의 동작은 &lt;code&gt;RecoveryCallback&lt;/code&gt;에 담아서 넘기는 형식입니다.
&lt;code&gt;RecoveryCallback&lt;/code&gt; 인터페이스도 메서드 1개만 단순하게 정의되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RecoveryCallback 인터페이스&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public interface RecoveryCallback&amp;lt;T&amp;gt; {
  T recover(RetryContext context) throws Exception;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NotificationRetryDecorator&lt;/code&gt;에서도 이를 활용해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecorator.send(String) 메서드에서 RecoveryCallback 지정&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	@Override
	public boolean send(String message) {
		return retryOperations.execute(
			(context) -&amp;gt; this.target.send(message),
			(context) -&amp;gt; recover(context.getLastThrowable(), message) // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		);
	}

	private boolean recover(Throwable error, String message) { // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		logger.warn(&quot;메시지 전송 최종 실패 후 recover : {}&quot;, message, error);
		return false;
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;같은 클래스의 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드를 호출하도록 &lt;code&gt;RecoveryCallback&lt;/code&gt;을 람다 표현식으로 정의했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;추가한 메서드입니다. 전송을 요청한 메시지와 예외를 로깅하고 &lt;code&gt;false&lt;/code&gt; 값을 반환합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NotificationRetryDecoratorTest&lt;/code&gt;에 메서드를 추가하여 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드가 의도대로 실행됨을 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;recover 동작 수행 테스트&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	@Test
	void recover() {
		var target = new UnstableNotificationService(2); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		var decorator = new NotificationRetryDecorator(target, 2); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;

		boolean success = decorator.send(&quot;hello&quot;);

		assertThat(success).isFalse(); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
		assertThat(target.getTryCount()).isEqualTo(2);
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;처음 2번째 호출까지는 예외를 내는 &lt;code&gt;NotificationService&lt;/code&gt;를 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2번까지 재시도하도록 &apos;decorator&amp;#8217;를 만듭니다. 결과적으로 모든 시도가 예외로 실패합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;마지막 시도가 실패했어도 예외가 발생하지 않고 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드가 실행되어 &lt;code&gt;false&lt;/code&gt; 값이 반환되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;추가한 테스트 메서드를 실행하면 로그 메시지를 통해서도 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드가 실행되었음이 확인됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RecoveryCallback&lt;/code&gt;으로 유도한 실행 흐름은 마치 &lt;code&gt;try - catch&lt;/code&gt; 구문의 &lt;code&gt;catch&lt;/code&gt; 블럭과 비슷한 역할을 합니다.
작업을 멈추는 것보다 차선의 기능으로 대체하는 것이 나은 동작이 있다면 이 구조를 활용해볼 만합니다.
예를 들면 사용자의 프로필 이미지를 HTTP 호출로 읽어오는 동작이 실패했을 때 디폴트 이미지로 대체하는 기능 같은 것들입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;circuitbreakerretrypolicy&quot;&gt;2.3. CircuitBreakerRetryPolicy&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryPolicy&lt;/code&gt; 구현체 중 하나인 &lt;code&gt;CircuitBreakerRetryPolicy&lt;/code&gt; 클래스는 실패하는 요청을 계속하지 않도록 하여 장애 확산을 방어하는 서킷 브레이커 개념을 지원합니다.
특정 시간 내에 특정 횟수의 시도가 실패하면 &apos;차단기&amp;#8217;가 열려서 한동안은 실제 동작을 수행하는 메서드를 호출하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예제를 통해 다음 정책을 구현해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;200ms 동안 지정된 횟수 이상 메서드 호출이 실패하면 차단기가 열린다. 차단기가 열려 있는 동안은 실제 기능을 수행하는 메서드를 호출하지 않고 리커버 동작만 수행한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;차단기는 300ms 동안 유지되고, 해당 시간이 지나면 닫힌다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서킷 브레이커가 열리고 다시 닫히는 시간이 정책에 들어갑니다.
앞에서 했던 단순히 횟수를 지정하는 정책과 다르게 호출이 실패한 시간 정보 등이 기록되어야 합니다.
&lt;code&gt;RetryTemplate&lt;/code&gt;에서는 내부적으로 &lt;code&gt;RetryContextCache&lt;/code&gt; 라는 인터페이스를 통해 상태 정보를 저장합니다.
기본값으로는 메모리에 상태를 저장하는 &lt;code&gt;MapRetryContextCache&lt;/code&gt; 구현체가 쓰입니다.
여러 서버 간에 재시도 기준 정보를 공유하고 싶다면 &lt;code&gt;RetryContextCache&lt;/code&gt;를 직접 구현해서 Redis 같은 글로벌 캐시에 저장하는 것도 가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이러한 상태 정보를 관리하는 데 필요한 키값 등은 &lt;code&gt;RetryState&lt;/code&gt; 인터페이스를 통해 전달됩니다.
기본 구현체로 &lt;code&gt;DefaultRetryState&lt;/code&gt; 클래스가 제공되고 이를 통해 다음의 속성을 전달합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;key : 재시도 상태를 관리하는 기준이 되는 키&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;forceRefresh : 완전히 새로운 상태로 시도할 경우 true&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rollbackClassifier : 트랜잭션 롤백을 위해 특정 예외를 전파할지를 결정하는 역할을 합니다. &lt;code&gt;org.springframework.classify.Classifier&lt;/code&gt; 인터페이스를 파라미터 타입으로 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryTemplate&lt;/code&gt; 클래스와 &lt;code&gt;RetryOperations&lt;/code&gt; 인터페이스에는 &lt;code&gt;RetryState&lt;/code&gt;를 포함하는 &lt;code&gt;execute(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드가 있습니다.
서킷 브레이커 정책을 적용하기 위해서는 이 메서드를 호출해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RetryState가 파라미터에 포함된 execute(&amp;#8230;&amp;#8203;) 메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	&amp;lt;T, E extends Throwable&amp;gt; T execute(RetryCallback&amp;lt;T, E&amp;gt; retryCallback, RecoveryCallback&amp;lt;T&amp;gt; recoveryCallback,
			RetryState retryState) throws E;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 메서드를 활용하여 서킷 브레이커 정책을 적용하는 예제도 Decorator 패턴으로 먼저 구현해보겠습니다.
앞선 실습의 &lt;code&gt;NotificationRetryDecorator&lt;/code&gt; 클래스와 구조가 거의 비슷하므로 복사해서 만들어도 좋습니다.
먼저 &lt;code&gt;RetryTemplate&lt;/code&gt;을 생성하는 &lt;code&gt;buildRetryOperations()&lt;/code&gt; 메서드부터 정의하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationCircuitDecorator.java에서 RetryTemplate을 생성하는 메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class NotificationCircuitDecorator implements NotificationService {
	// 중간 생략

	private RetryOperations buildRetryOperations(int maxAttempts) {
		var simplePolicy = new SimpleRetryPolicy(
			maxAttempts,
			Map.of(RuntimeException.class, true)
		);
		var circuitBreakerPolicy = new CircuitBreakerRetryPolicy(simplePolicy); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		circuitBreakerPolicy.setOpenTimeout(200L); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		circuitBreakerPolicy.setResetTimeout(300L); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;

		var retryTemplate = new RetryTemplate();
		retryTemplate.setRetryPolicy(circuitBreakerPolicy); // &lt;b class=&quot;conum&quot;&gt;(4)&lt;/b&gt;
		return retryTemplate;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;서킷 브레이커의 발동 조건이 되는 대상 예외와 발동 전 최대 호출 횟수는 &lt;code&gt;SimpleRetryPolicy&lt;/code&gt;로 정의해서, &lt;code&gt;CircuitBreakerRetryPolicy&lt;/code&gt; 내부에서 참조하도록 생성자의 인자로 전달합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;200ms 동안 &lt;code&gt;SimpleRetryPolicy&lt;/code&gt;에서 지정된 횟수의 시도가 실패하면 서킷 브레이커가 발동합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;300ms가 지나면 서킷 브레이커가 닫혀서 다시 원래의 대상 메서드를 호출합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;앞에서 구성한 정책을 &lt;code&gt;RetryTemplate&lt;/code&gt;에 넣습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NotificationCircuitDecorator&lt;/code&gt;의 나머지 부분도 완성합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationCircuitDecorator.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class NotificationCircuitDecorator implements NotificationService {

	private final Logger logger = LoggerFactory.getLogger(NotificationCircuitDecorator.class);
	private final NotificationService target;
	private final RetryOperations retryOperations;
	private final RetryState state; // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;

	public NotificationCircuitDecorator(NotificationService target, int maxAttempts) {
		this.target = target;
		this.retryOperations = buildRetryOperations(maxAttempts);
		String stateKey = &quot;NotificationService.send&quot;; // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		var rollbackClassifier = new BinaryExceptionClassifier(false); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
		this.state = new DefaultRetryState(stateKey, rollbackClassifier);
	}

	@Override
	public boolean send(String message) {
		return this.retryOperations.execute(
			(context) -&amp;gt; this.target.send(message),
			(context) -&amp;gt; recover(context.getLastThrowable(), message),
			this.state // &lt;b class=&quot;conum&quot;&gt;(4)&lt;/b&gt;
		);
	}
	// recover 메서드는 NotificationRetryDecorator의 것과 동일
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;NotificationRetryDecorator&lt;/code&gt; 클래스 대비 추가된 멤버 변수입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도를 위한 상태 정보의 키를 재시도할 메서드의 이름으로 썼습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;원래의 메서드를 호출해서 생기는 예외를 다시 전파하지 않도록 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;상태를 유지하는 재시도를 위해 여러 &lt;code&gt;execute(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드 중 &lt;code&gt;RetryState&lt;/code&gt;를 파라미터로 받는 것을 호출합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트 코드로 서킷 브레이커가 잘 동작하는지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationCircuitDecoratorTest.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class NotificationCircuitDecoratorTest {
	UnstableNotificationService target = new UnstableNotificationService(2); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
	NotificationCircuitDecorator decorator = new NotificationCircuitDecorator(target, 2); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;

	private void callAndAssert(boolean expectedResult, int expectedTryCount) { // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
		boolean success = decorator.send(&quot;Hello!&quot;);
		assertThat(success).isEqualTo(expectedResult);
		assertThat(target.getTryCount()).isEqualTo(expectedTryCount);
	}

	@Test
	void recoveredWithCircuit() throws InterruptedException {
		callAndAssert(false, 1); // &lt;b class=&quot;conum&quot;&gt;(4)&lt;/b&gt;
		callAndAssert(false, 2);
		callAndAssert(false, 2); // &lt;b class=&quot;conum&quot;&gt;(5)&lt;/b&gt;
		TimeUnit.MILLISECONDS.sleep(310); // &lt;b class=&quot;conum&quot;&gt;(6)&lt;/b&gt;
		callAndAssert(true, 3); // &lt;b class=&quot;conum&quot;&gt;(7)&lt;/b&gt;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;처음 2번은 예외를 내는 target 객체를 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2번 호출이 실패하면 서킷이 열리는 decorator 객체를 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;반복되는 호출과 검증 동작을 private 메서드로 추출했습니다. 첫 번째 파라미터는 &lt;code&gt;decorator.send(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드의 기대 결과값이고, 두 번째는 &lt;code&gt;target.send(String)&lt;/code&gt; 메서드가 실제 호출된 누적 횟수입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;첫 호출에도 리커버 메서드가 호출되어서 &lt;code&gt;decorator.send(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드에서는 &lt;code&gt;false&lt;/code&gt;를 반환했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;세 번째 시도이지만 서킷이 열려서 &lt;code&gt;target.send(String)&lt;/code&gt; 메서드의 실제 호출 횟수는 2에서 더 증가하지 않았습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;서킷이 닫히는 상태가 될 때까지 대기합니다. &lt;code&gt;circuitBreakerPolicy.setResetTimeout(300L)&lt;/code&gt; 코드로 지정한 시간보다 긴 310ms를 기다립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;다시 &lt;code&gt;target.send(String)&lt;/code&gt;가 실행되어 누적 호출 횟수가 3으로 증가했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;특정 횟수를 단순히 재시도했던 &lt;code&gt;SimpleRetryPolicy&lt;/code&gt;와는 최대 호출 횟수와 리커버 동작의 실행 시점 등 전체적인 흐름이 다르다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;애너테이션과_aop를_이용한_재시도&quot;&gt;3. 애너테이션과 AOP를 이용한 재시도&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;직접 &lt;code&gt;RetryTemplate&lt;/code&gt;을 호출하지 않고도 애너테이션과 AOP를 이용해서 재시도를 할 수 있습니다.
&lt;code&gt;@Retryable&lt;/code&gt;, &lt;code&gt;@Recover&lt;/code&gt;, &lt;code&gt;@CircuitBreaker&lt;/code&gt;가 여기에 쓰이는 애너테이션입니다.
이를 통해 앞에서 만든 Decorator 클래스들로 구성한 재시도 정책을 동일하게 적용하면서도 더 짧은 코드로 구현해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;먼저 &lt;code&gt;build.gradle&lt;/code&gt;의 &lt;code&gt;dependencies&lt;/code&gt; 단락에 다음과 같이 &lt;code&gt;spring-aspects&lt;/code&gt; 모듈을 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle에서 spring-aspects 모듈 의존성 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;    implementation &apos;org.springframework:spring-aspects&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Retry의 AOP 관련 기능이 &lt;code&gt;aspectjweaver&lt;/code&gt; 모듈을 런타임에 의존하고 있기 때문입니다.
위 선언이 없으면 이어질 실습을 진행할 때 &lt;code&gt;org.aspectj.lang.annotation.Pointcut&lt;/code&gt; 클래스를 찾을 수 없다는 에러가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;retryable&quot;&gt;3.1. @Retryable&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@Retryable&lt;/code&gt;을 붙인 인터페이스를 &lt;code&gt;NotificationRetryService&lt;/code&gt;로 정의하겠습니다.
앞 단락의 예제를 보존하기 위해서 &lt;code&gt;NotificationService&lt;/code&gt;를 상속한 인터페이스를 따로 만들었습니다.
&lt;code&gt;NotificationService&lt;/code&gt;와 같은 패키지에 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;@Retryable을 활용한 NotificationRetryService.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;

interface NotificationRetryService extends NotificationService {
	@Retryable(
		retryFor = RuntimeException.class, maxAttempts = 4,
		backoff = @Backoff(delay = 200L, multiplier = 2d, maxDelay = 600L)
	)
	boolean send(String message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryTemplate.builder()&lt;/code&gt; 로 했을 때와 유사하게 애너테이션의 속성으로 대상이 되는 예외, 재시도 횟수와 BackOff 정책을 정의했습니다.
&lt;code&gt;@Retryable&lt;/code&gt; 애너테이션은 구현 클래스에 직접 붙일 수도 있습니다.
이 실습 프로젝트에서는 실제 원격 호출로 알림을 전송하는 구현 클래스가 상상 속에만 존재하기 때문에 인터페이스에 붙였습니다.
테스트용 구현체인 &lt;code&gt;UnstableNotificationService&lt;/code&gt; 클래스에 직접 &lt;code&gt;@Retryable&lt;/code&gt;을 붙이는 것도 자연스럽지 않습니다.
인터페이스를 따로 정의했기에 이를 구현한 여러 클래스에 동일한 재시도 정책을 적용할 수 있다는 장점이 생겼습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트를 위해서 아래와 같이 &lt;code&gt;UnstableNotificationService&lt;/code&gt;가 구현하는 인터페이스를 &lt;code&gt;NotificationRetryService&lt;/code&gt;로 바꿉니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;UnstableNotificationService의 implements 절 변경&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class UnstableNotificationService implements NotificationRetryService {&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;의도대로 재시도가 되는지 다시 테스트 코드로 확인하겠습니다.
원래의 &lt;code&gt;UnstableNotificationService&lt;/code&gt; 클래스의 생성자로 바로 만든 객체가 아닌, 스프링의 AOP에 의해서 재시도 기능이 덧붙여진 객체를 만들어야 합니다.
Spring Retry에서 제공하는 &lt;code&gt;@EnableRetry&lt;/code&gt; 애너테이션이 이를 위해 필요합니다.
그리고 &lt;code&gt;UnstableNotificationService&lt;/code&gt;를 애플리케이션 컨텍스트에 등록해서 스프링 컨테이너가 이 클래스에 AOP를 적용할 기회를 주어야 합니다.
이를 위해 테스트 전용 애플리케이션 컨텍스트를 구성하는 &lt;code&gt;TestServiceConfig.java&lt;/code&gt; 파일을 &apos;src/test/java&apos; 디렉터리의 하위 경로에 추가합니다.
실제 운영에 쓰일 코드도 동일한 방식으로 적용이 가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;AOP 테스트를 위한 애플리케이션 컨텍스트 정의&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Configuration
@EnableRetry(proxyTargetClass = true) // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
public class TestServiceConfig {
	@Bean
	public UnstableNotificationService fail3Service() {
		return new UnstableNotificationService(3); // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@Retryable&lt;/code&gt; 애너테이션이 붙은 클래스가 애플리케이션 컨텍스트에 등록되면 재시도 기능을 추가한 프록시 객체를 만들어줍니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3번째 시도까지 예외를 내는 테스트용 객체를 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 예제에서는 &lt;code&gt;proxyTargetClass = true&lt;/code&gt; 속성을 통해서 인터페이스가 아닌 클래스를 확장한 프록시 객체로 AOP가 적용되도록 유도했습니다.
테스트에서 &lt;code&gt;UnstableNotificationService&lt;/code&gt; 클래스 수준에서 정의된 &lt;code&gt;getTryCount()&lt;/code&gt; 메서드를 활용하기 위해서입니다.
이 속성이 없으면 인터페이스 기반의 JDK 다이내믹 프록시가 생성되어, 인터페이스에 정의되지 않은 메서드는 프록시 객체에서 호출할 수 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;TestServiceConfig&lt;/code&gt;에서 정의한 애플리케이션 컨텍스트를 올려서 AOP가 적용된 &lt;code&gt;UnstableNotificationService&lt;/code&gt;를 테스트하는 코드를 만들어보겠습니다.
&lt;code&gt;NotificationRetryServiceTest.java&lt;/code&gt; 파일로 이름을 붙였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryServiceTest에서 AOP가 적용된 객체를 대상으로 테스트&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@SpringJUnitConfig(TestServiceConfig.class) // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
class NotificationRetryServiceTest {
	@Test
	void retry(@Qualifier(&quot;fail3Service&quot;) UnstableNotificationService fail3Service) { // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		boolean success = fail3Service.send(&quot;Hello!&quot;);
		assertThat(fail3Service.getTryCount()).isEqualTo(4); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
		assertThat(success).isTrue();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;앞서 만든 테스트용 애플리케이션 컨텍스트를 참조합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이후에 같은 타입으로 여러 개의 빈(bean)을 등록할 계획이 있기에 &lt;code&gt;@Qualifier&lt;/code&gt;로 이름을 명시했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;send(String)&lt;/code&gt; 메서드를 테스트에서 직접 호출한 것은 1번이지만 재시도가 3번 되어서 총 4번 실행되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 결과를 보면 &lt;code&gt;implements NotificationRetryService&lt;/code&gt; 선언만으로, &lt;code&gt;NotificationRetryDecorator&lt;/code&gt; 클래스에서 직접 &lt;code&gt;RetryTemplate&lt;/code&gt;을 써서 재시도 정책을 정의한 것과 동일한 효과를 낸다는 것을 알 수 있습니다.
Decorator 클래스를 Spring Retry 모듈이 실행 시점에 자동으로 만들어준다고 이해할 수 있습니다.
Spring Retry 안의 &lt;code&gt;RetryOperationsInterceptor&lt;/code&gt; 클래스가 내부적으로 그런 역할을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;recover&quot;&gt;3.2. @Recover&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RecoveryCallback&lt;/code&gt; 안에 담겨서 호출될 메서드도 &lt;code&gt;@Recover&lt;/code&gt; 애너테이션으로 지정할 수 있습니다.
&lt;code&gt;NotificationRetryService&lt;/code&gt; 인터페이스에 &lt;code&gt;false&lt;/code&gt; 값을 반환하는 디폴트 메서드를 추가하고 그 위에 &lt;code&gt;@Recover&lt;/code&gt;를 붙입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryService에 @Recover 메서드 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	Logger logger = LoggerFactory.getLogger(NotificationRetryService.class);

	@Recover // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
	default boolean recover(Throwable error, String message) { // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		logger.warn(&quot;메시지 전송 최종 실패 후 recover : {}&quot;, message, error);
		return false;
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;최종 재시도가 실패할 시 대신 호출될 메서드로 지정합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;마지막 재시도에서 발생한 예외와, 재시도 대상 메서드로 넘어왔던 파라미터의 값들을 전달받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@Recover&lt;/code&gt; 가 붙은 메서드의 첫 번째 파라미터를 &lt;code&gt;Throwable&lt;/code&gt; 타입으로 선언하면 &lt;code&gt;@Retryable&lt;/code&gt; 이 붙은 메서드에서 발생한 에러 객체를 전달받을 수 있습니다.
이후의 파라미터들은 &lt;code&gt;@Retryable&lt;/code&gt; 이 붙은 메서드의 파라미터를 순서대로 선언하면 같은 값을 넘겨받을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@Recover&lt;/code&gt;를 붙인 메서드가 의도대로 호출되는지 테스트로 확인해보겠습니다.
앞서 &lt;code&gt;NotificationRetryService.send(String)&lt;/code&gt; 메서드는 &lt;code&gt;@Retryable&lt;/code&gt; 선언으로 총 4번까지 시도를 하므로, 호출이 4번까지 실패해야 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드가 호출됩니다.
이 테스트 시나리오를 위해 일부러 4번까지 예외를 내는 &lt;code&gt;UnstableNotificationService&lt;/code&gt;를 애플리케이션 컨텍스트에 등록합니다. &lt;code&gt;TestServiceConfig.java&lt;/code&gt;에 메서드를 추가하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;TestServiceConfig.java에서 4번 실패하는 UnstableNotificationService 등록&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	@Bean
	public UnstableNotificationService fail4Service() {
		return new UnstableNotificationService(4);
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NotificationRetryServiceTest&lt;/code&gt;에서 위에서 만든 &apos;fail4Service&amp;#8217;를 호출해서 4번 재시도되는 결과를 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryServiceTest에 recover까지 검증하는 테스트 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	@Test
	void retryAndRecover(@Qualifier(&quot;fail4Service&quot;) UnstableNotificationService fail4Service) {
		boolean success = fail4Service.send(&quot;Hello!&quot;);
		assertThat(fail4Service.getTryCount()).isEqualTo(4);
		assertThat(success).isFalse(); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;예외 대신 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드의 결과인 &lt;code&gt;false&lt;/code&gt; 값이 반환됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트를 실행하면 로그 메시지 중에서도 &apos;메시지 전송 최종 실패 후 recover&amp;#8217;라는 문자열이 보일 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;circuitbreaker&quot;&gt;3.3. @CircuitBreaker&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마지막으로 &lt;code&gt;@CircuitBreaker&lt;/code&gt; 애너테이션을 알아보겠습니다.
앞에 나왔던 &lt;code&gt;CircuitBreakerRetryPolicy&lt;/code&gt;를 활용했던 &lt;code&gt;NotificationCircuitDecorator&lt;/code&gt; 예제와 동일한 기능을 더 간결한 코드로 만들 수 있습니다.
&lt;code&gt;NotificationCircuitService&lt;/code&gt;라는 새로운 인터페이스를 정의해서 &lt;code&gt;@CircuitBreaker&lt;/code&gt;의 속성으로 대상 예외와 타임아웃 시간 등을 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;@CircuitBreaker를 붙인 NotificationCircuitService 인터페이스&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;interface NotificationCircuitService {

	@CircuitBreaker(
		retryFor = RuntimeException.class, // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
		maxAttempts = 2, openTimeout = 200L, resetTimeout = 300L // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
	)
	boolean sendOnCircuit(String message); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RuntimeException&lt;/code&gt;이 발생한 경우를 서킷 브레이커가 발동될 대상으로 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;최대 2회 시도 후 실패하면 서킷이 열리고, 300ms가 지나면 서킷이 닫혀서 다시 실제 재시도 대상 메서드가 호출됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;메서드명도 앞선 예제와 구분하기 위해 &lt;code&gt;sendOnCircuit(String)&lt;/code&gt;으로 다르게 붙였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트로 이 동작을 확인해볼 &lt;code&gt;FragileService&lt;/code&gt;라는 클래스를 만들겠습니다.
&lt;code&gt;UnstableNotificationService&lt;/code&gt;와 동일한 동작을 하는 클래스가 필요한데, 앞선 예제를 보존하기 위해 클래스를 하나 더 만듭니다.
메서드의 구현부는 동일하므로 실습 예제의 간결함을 위해 &lt;code&gt;UnstableNotificationService&lt;/code&gt; 클래스를 상속하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;FragileService.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class FragileService extends UnstableNotificationService implements NotificationCircuitService {
	public FragileService(int failures) {
		super(failures);
	}

	@Override
	public boolean sendOnCircuit(String message) {
		return super.send(message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;AOP가 적용되도록 앞선 실습에서 만들어 둔 &lt;code&gt;TestServiceConfig&lt;/code&gt; 클래스에서 &lt;code&gt;FragileService&lt;/code&gt; 클래스의 인스턴스를 &lt;code&gt;@Bean&lt;/code&gt;으로 등록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;FragileService를 bean으로 등록&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Bean
    public FragileService fail2Service() {
      return new FragileService(2); // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;두 번은 의도적인 에러를 내도록 &lt;code&gt;FragileService&lt;/code&gt;의 인스턴스를 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;애플리케이션 컨텍스트가 로딩되는 순간 &apos;fail2Service&apos; 객체를 감싼, 서킷 브레이커 정책이 적용된 객체가 자동으로 생성됩니다.
Spring Retry 내부의 &lt;code&gt;StatefulRetryOperationsInterceptor&lt;/code&gt; 클래스가 그 역할을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Decorator로 서킷 브레이커를 적용했던 &lt;code&gt;NotificationCircuitDecoratorTest&lt;/code&gt;와 동일한 시나리오로 테스트를 해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationCircuitServiceTest.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@SpringJUnitConfig(TestServiceConfig.class) // &lt;b class=&quot;conum&quot;&gt;(1)&lt;/b&gt;
class NotificationCircuitServiceTest {

	@Autowired
	FragileService fail2Service;

	private void callAndAssert(boolean expectedResult, int expectedTryCount) { // &lt;b class=&quot;conum&quot;&gt;(2)&lt;/b&gt;
		boolean success = fail2Service.sendOnCircuit(&quot;Hello!&quot;);
		assertThat(success).isEqualTo(expectedResult);
		assertThat(fail2Service.getTryCount()).isEqualTo(expectedTryCount);
	}

	@Test
	void recoveredWithCircuit() throws InterruptedException {
		callAndAssert(false, 1); // &lt;b class=&quot;conum&quot;&gt;(3)&lt;/b&gt;
		callAndAssert(false, 2);
		callAndAssert(false, 2); // &lt;b class=&quot;conum&quot;&gt;(4)&lt;/b&gt;
		TimeUnit.MILLISECONDS.sleep(310); // &lt;b class=&quot;conum&quot;&gt;(5)&lt;/b&gt;
		callAndAssert(true, 3); // &lt;b class=&quot;conum&quot;&gt;(6)&lt;/b&gt;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;colist arabic&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;AOP가 적용된 &lt;code&gt;FragileService&lt;/code&gt;를 참조할 수 있도록 애플리케이션 컨텍스트를 로딩합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;NotificationCircuitDecoratorTest&lt;/code&gt; 예제의 것과 동일한 메서드입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;실제 &lt;code&gt;sendOnCircuit(String)&lt;/code&gt; 메서드 호출에서는 예외가 났지만 &lt;code&gt;recover(&amp;#8230;&amp;#8203;)&lt;/code&gt; 가 실행되어서 반환값은 &lt;code&gt;false&lt;/code&gt;가 나옵니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;차단기가 열린 이후부터는 더 이상 원래의 &lt;code&gt;sendOnCircuit(String)&lt;/code&gt; 메서드가 호출되지 않아서 호출 횟수가 증가하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@CircuitBreaker&lt;/code&gt;의 &lt;code&gt;resetTimeout&lt;/code&gt; 속성값보다 큰 시간이 지나갑니다. 이후로는 차단기가 닫힙니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;더 이상 호출이 차단되지 않아서 원래의 &lt;code&gt;sendOnCircuit(String)&lt;/code&gt; 메서드가 실행되고 실행 횟수가 증가합니다. 의도한 2번의 예외가 발생한 이후이므로 호출도 성공합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;retrylistener&quot;&gt;4. RetryListener&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryListener&lt;/code&gt;를 &lt;code&gt;RetryTemplate&lt;/code&gt;에 지정하면 재시도 과정의 각 시점에 개입할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryListener&lt;/code&gt; 인터페이스에는 다음과 같이 정의되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RetryListener 인터페이스&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package org.springframework.retry;

public interface RetryListener {
  default &amp;lt;T, E extends Throwable&amp;gt; boolean open(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback) {}
  default &amp;lt;T, E extends Throwable&amp;gt; void onError(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback, Throwable throwable) {}
  default &amp;lt;T, E extends Throwable&amp;gt; void close(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback, Throwable throwable) {}
  default &amp;lt;T, E extends Throwable&amp;gt; void onSuccess(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback, T result) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 메서드들을 활용하면 재시도 사이클이 시작되는 시점, 재시도 중 에러가 나는 시점, 성공하는 시점, 재시도가 끝나는 시점에 개입하여 확장된 기능을 넣을 수 있습니다.
예를 들면 특정 조건에 따라서 재시도를 하지 않거나 멈추는 것도 가능합니다.
&lt;code&gt;RetryListener.open(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드에서 &lt;code&gt;false&lt;/code&gt;를 반환하거나 &lt;code&gt;onError(&amp;#8230;&amp;#8203;)&lt;/code&gt; 안에서 &lt;code&gt;RetryContext.setExhaustedOnly()&lt;/code&gt; 메서드를 호출하면 재시도는 더 이상 진행되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryListener&lt;/code&gt;의 메서드가 호출되는 시점을 확인하는 예제를 만들어보겠습니다.
&lt;code&gt;RetryLoggingListener&lt;/code&gt; 라는 이름으로 각 메서드에서 로그 메시지를 남기는 클래스를 구현합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RetryLoggingListener.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;

public class RetryLoggingListener implements RetryListener {
	private final Logger logger = LoggerFactory.getLogger(RetryLoggingListener.class);

	@Override
	public &amp;lt;T, E extends Throwable&amp;gt; boolean open(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback) {
		logger.info(&quot;open : {}&quot;, context);
		return true;
	}

	@Override
	public &amp;lt;T, E extends Throwable&amp;gt; void onError(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback,
		Throwable throwable) {
		logger.warn(&quot;onError: {}&quot;, context, throwable);
	}

	@Override
	public &amp;lt;T, E extends Throwable&amp;gt; void close(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback,
		Throwable throwable) {
		logger.info(&quot;close : {}&quot;, context);
	}

	@Override
	public &amp;lt;T, E extends Throwable&amp;gt; void onSuccess(RetryContext context, RetryCallback&amp;lt;T, E&amp;gt; callback, T result) {
		logger.info(&quot;onSuccess : {}&quot;, result);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이번 글에서 만든 &lt;code&gt;NotificationRetryDecorator&lt;/code&gt;에 위의 리스너를 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecorator에서 리스너 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	public NotificationRetryDecorator(NotificationService target, int maxAttempts) {
		this.target = target;
		this.retryOperations = RetryTemplate.builder()
            // 중략
			.withListener(new RetryLoggingListener()) // 추가된 라인
			.build();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;RetryTemplate.setListeners(&amp;#8230;&amp;#8203;)&lt;/code&gt; 메서드를 호출하는 방법으로도 리스너를 지정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 상황을 비교하기 위해 &lt;code&gt;NotificationRetryDecoratorTest&lt;/code&gt;에 실패 없이 바로 성공하는 테스트 메서드도 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NotificationRetryDecoratorTest에서 한 번에 성공하는 테스트 케이스 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	@Test
	void successOnFirstTry() {
		NotificationService target = (message) -&amp;gt; {
			System.out.println(&quot;Message : &quot; + message);
			return true;
		};
		var decorator = new NotificationRetryDecorator(target, 2);

		boolean success = decorator.send(&quot;hello&quot;);
		assertThat(success).isTrue();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 테스트를 실행하면 콘솔에서 &lt;code&gt;RetryLoggingListener&lt;/code&gt;가 남기는 로그 메시지가 보일 것입니다.
이를 통해 &lt;code&gt;RetryContext&lt;/code&gt;가 어떤 정보를 담고 있는지도 대략 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;NotificationRetryDecoratorTest&lt;/code&gt;의 여러 메서드를 통해서 다음을 확인해볼 수 있습니다.
한 번에 타깃 메서드 호출이 성공하는 경우는 다음과 같이 &lt;code&gt;RetryListener&lt;/code&gt;의 메서드가 호출됩니다.
(&lt;code&gt;NotificationRetryDecoratorTest.successOnFirstTry()&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.open(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도 대상 호출&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.onSuccess(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.close(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한 번 이상 실패했지만 재시도로 성공한 경우의 호출 순서는 다음과 같습니다.
(&lt;code&gt;NotificationRetryDecoratorTest.successByRetry()&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.open(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도 대상 메서드 호출했으나 실패&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.onError(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2 ~ 3번 과정을 성공할 때까지 반복&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.onSuccess(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.close(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;재시도를 했어도 최종 실패하고 리커버 메서드가 실행되면 다음과 같습니다.
(&lt;code&gt;NotificationRetryDecoratorTest.recover()&lt;/code&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.open(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;재시도 대상 메서드 호출했으나 실패&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.onError(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2 ~ 3번 과정을 최대 재시도 허용 횟수까지 반복&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;리커버 메서드 실행&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RetryListener.close(&amp;#8230;&amp;#8203;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Batch에서도 &lt;code&gt;StepBuilder.faultTolerant()&lt;/code&gt;와 &lt;code&gt;listener(RetryListener)&lt;/code&gt;를 호출해서 재시도 리스너를 가진 스텝을 구성할 수 있습니다.
그런 스텝에서 참조되는 &lt;code&gt;RetryListener&lt;/code&gt;의 메서드는 &lt;code&gt;ItemProcessor&lt;/code&gt;, &lt;code&gt;ItemWriter&lt;/code&gt;의 호출 전후에 2번 호출된다는 점도 유의해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;5. 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글에서는 &lt;code&gt;RetryTemplate&lt;/code&gt;을 직접 호출하는 방식과 AOP를 통한 방식, 두 가지 방법으로 재시도와 리커버, 서킷 브레이커 정책을 적용해보았습니다.
AOP가 편리한 면이 있지만 트랜잭션 등 다른 AOP와 함께 적용되면 적용 순서 등도 신경 써야 합니다.
재시도를 명시적인 코드로 표현하기 위해 직접 &lt;code&gt;RetryTemplate&lt;/code&gt;을 호출하는 방법도 유용할 때가 있습니다.
AOP를 쓰더라도 &lt;code&gt;RetryTemplate&lt;/code&gt;과 그와 연관된 콜백 인터페이스, 재시도 정책 구현체를 잘 알고 있으면 의도대로 정책이 적용되지 않을 때 문제를 빠르게 파악할 수 있습니다.
Spring Retry 내부에서 재시도가 적용되는 프록시 객체를 만들어주는 &lt;code&gt;RetryOperationsInterceptor&lt;/code&gt;, &lt;code&gt;StatefulRetryOperationsInterceptor&lt;/code&gt; 클래스에 브레이크 포인트를 걸어서 &apos;retryOperations&apos; 멤버 변수에 어떤 정책이 어떤 속성으로 지정되었는지 추적해보는 문제 해결 방법도 유용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실패 후 재시도 등의 동작이 의도대로 동작하는지를 확인하는 테스트도 중요합니다.
평소가 아닌 문제나 장애가 발생할 때를 대비한 코드이므로, 의식적으로 실패 상황을 만들지 않는다면 제대로 동작할지 알 수 없습니다.
검증되지 않은 방어 코드는 실제 상황에서 오히려 문제를 더 심각하게 만들기도 합니다.
다양한 방법으로 테스트를 해야겠지만, 이 글에서 소개한 테스트용 가짜 객체를 만드는 방법도 선택지 중의 하나입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;6. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-retry&quot;&gt;Spring Retry GitHub 저장소&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/resilience.html&quot;&gt;Spring Framework Resilience Features&lt;/a&gt; — Spring Framework 7.0에 흡수된 재시도 기능 문서&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Git의 무시 규칙과 비밀 관리 도구: .gitignore부터 SOPS, gitleaks까지</title>
      <link>https://blog.benelog.net//git-ignore-and-secrets.html</link>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">git-ignore-and-secrets.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#gitignore_vs_gitinfoexclude&quot;&gt;1. .gitignore vs .git/info/exclude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#git_secret&quot;&gt;2. git-secret&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sops_secrets_operations&quot;&gt;3. SOPS (Secrets OPerationS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#gitleaks&quot;&gt;4. gitleaks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리_무엇을_쓸까&quot;&gt;5. 정리: 무엇을 쓸까&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;6. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;API 키나 &lt;code&gt;.env&lt;/code&gt; 파일 같은 비밀을 Git 저장소에서 다루는 방법은 크게 세 가지입니다. 아예 추적하지 않도록 무시하기, 암호화해서 커밋하기, 실수로 커밋되는 것을 탐지하기입니다. 각 방법마다 다른 도구가 발전해 왔습니다. 이 글에서는 Git의 무시 규칙인 `.gitignore`와 `.git/info/exclude`의 차이에서 출발해, 암호화 커밋 도구인 git-secret과 SOPS, 유출 스캐너인 gitleaks까지 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gitignore_vs_gitinfoexclude&quot;&gt;1. .gitignore vs .git/info/exclude&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘 다 같은 문법으로 무시할 파일 패턴을 적지만, &lt;strong&gt;공유 여부&lt;/strong&gt;가 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;code&gt;.git/info/exclude&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;커밋 여부&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;커밋됨(공유)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;커밋 안 됨(로컬 전용)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;적용 범위&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;클론한 모두&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;내 클론만&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;용도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;프로젝트 공통 무시 규칙&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;개인적인 무시 규칙&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/strong&gt;: 저장소에 커밋되어 모든 협업자에게 적용됩니다. 빌드 결과물, &lt;code&gt;node_modules/&lt;/code&gt; 등 &quot;누가 클론하든 무시해야 하는 것&quot;을 적습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.git/info/exclude&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;.git/&lt;/code&gt; 안에 있어 커밋되지 않고 내 로컬 클론에만 적용됩니다. 개인 메모, 로컬 도구가 만드는 파일처럼 &quot;나만 무시하고 싶은데 팀의 `.gitignore`를 더럽히긴 싫은 것&quot;을 적습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그 밖에 &lt;code&gt;git config core.excludesFile`로 지정하는 전역 무시 파일(보통 `~/.config/git/ignore&lt;/code&gt;)도 있습니다. 이 파일은 내 머신의 모든 저장소에 적용됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;셋 사이에 우선순위나 효과 차이는 없습니다. 셋 다 이미 추적 중인(tracked) 파일에는 영향을 주지 않고, 아직 추적되지 않은 파일만 무시합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;git_secret&quot;&gt;2. git-secret&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://sobolevn.me/git-secret/&quot;&gt;git-secret&lt;/a&gt;은 저장소 안에 비밀 파일(API 키, &lt;code&gt;.env&lt;/code&gt;, 인증서 등)을 &lt;strong&gt;암호화한 채로 커밋&lt;/strong&gt;할 수 있게 해주는 서드파티 도구입니다. Git 자체 기능이 아니라 별도로 설치하는 bash 기반 도구이며, GPG 공개키 암호화를 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;동작_방식&quot;&gt;2.1. 동작 방식&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;git secret init                 # .gitsecret/ 디렉터리 생성
git secret tell you@example.com # 복호화를 허용할 사람의 GPG 공개키 등록
git secret add .env             # 비밀 파일 지정(원본은 자동으로 .gitignore에 추가)
git secret hide                 # 등록된 공개키로 암호화한 .env.secret 생성 → 이것만 커밋
git secret reveal               # 클론한 팀원이 자기 GPG 개인키로 원본 복원&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저장소가 공개되어도 암호화본만 노출되고, 등록된 GPG 키 소유자만 풀 수 있습니다. 팀원을 빼려면 &lt;code&gt;git secret killperson&lt;/code&gt; 후 다시 &lt;code&gt;hide&lt;/code&gt; 합니다. 단, 과거 커밋 히스토리의 암호화본은 그 사람이 여전히 풀 수 있으므로 &lt;strong&gt;비밀 자체를 교체&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;이름이_비슷한_다른_것들&quot;&gt;2.2. 이름이 비슷한 다른 것들&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;git-secrets&lt;/code&gt;&lt;/strong&gt; (AWS 제작, 복수형): 정반대 목적입니다. 커밋 내용에 AWS 키 같은 비밀이 실수로 들어가는 것을 막는 pre-commit 검사기입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GitHub Secrets&lt;/strong&gt;: GitHub Actions에서 쓰는 암호화된 환경 변수 저장 기능입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;한계&quot;&gt;2.3. 한계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GPG 키 관리 부담이 크고, 파일 전체를 통째로 암호화하므로 diff와 코드 리뷰가 불가능합니다. 요즘은 SOPS나 플랫폼 환경 변수(Vercel, Supabase 등), 시크릿 매니저(1Password, Doppler 등) 쪽이 더 널리 쓰입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sops_secrets_operations&quot;&gt;3. SOPS (Secrets OPerationS)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/getsops/sops&quot;&gt;SOPS&lt;/a&gt;는 2015년 Mozilla에서 시작해 2023년 CNCF에 기증된(Sandbox 프로젝트) 암호화 파일 편집 도구입니다. 현재는 getsops 커뮤니티가 관리하며, 2026년 6월 30일 릴리스된 3.13.2가 최신입니다(2026년 8월 기준).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;git_secret과의_결정적_차이_값만_암호화&quot;&gt;3.1. git-secret과의 결정적 차이: 값만 암호화&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SOPS는 YAML, JSON, ENV, INI, BINARY 형식을 지원하며, 구조화된 파일에서는 &lt;strong&gt;키는 평문으로 두고 값만 암호화&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;# SOPS로 암호화한 YAML — 구조가 보인다
database:
    host: ENC[AES256_GCM,data:Tr7o...,type:str]
    password: ENC[AES256_GCM,data:CwE4...,type:str]
sops:
    age: ...
    lastmodified: &quot;2026-08-01T...&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 어떤 설정 항목이 바뀌었는지 diff와 코드 리뷰가 가능합니다. 파일 전체가 불투명한 blob이 되는 git-secret과의 가장 큰 차이입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;키_백엔드_age_pgp_kms_vault&quot;&gt;3.2. 키 백엔드: age, PGP, KMS, Vault&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GPG에 묶인 git-secret과 달리 네 계열의 키 관리 방식을 지원합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;age&lt;/strong&gt; (개인·소규모 팀에 권장 — GPG보다 훨씬 단순한 현대적 파일 암호화 도구)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PGP(GPG)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HashiCorp Vault&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;클라우드 KMS를 쓰면 IAM으로 접근을 제어하므로, &quot;퇴사자가 과거 히스토리를 풀 수 있는&quot; 문제도 키 접근 권한 회수로 해결됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;기본_사용_흐름_age_기준&quot;&gt;3.3. 기본 사용 흐름 (age 기준)&lt;/h3&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;`age-keygen -o ~/.config/sops/age/keys.txt`로 키 쌍을 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`.sops.yaml`에 어떤 파일을 어떤 키로 암호화할지 규칙을 선언합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`sops secrets/prod.yaml`로 파일을 편집하면 저장할 때 자동으로 암호화됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.sops.yaml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;creation_rules:
  - path_regex: secrets/.*\.yaml
    age: age1abc...  # 공개키&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sops secrets/prod.yaml        # 편집기가 열리고, 저장하면 자동 암호화
sops -d secrets/prod.yaml     # 복호화 출력
sops -r -i secrets/prod.yaml  # 데이터 키 회전&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;encrypted_regex&lt;/code&gt; 옵션으로 특정 키(예: &lt;code&gt;password|token&lt;/code&gt;)의 값만 골라 암호화하는 부분 암호화도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;생태계&quot;&gt;3.4. 생태계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kubernetes GitOps 도구와 통합이 잘 되어 있습니다. Flux CD는 SOPS를 기본 지원하고, ArgoCD·Helm(helm-secrets)·Ansible(community.sops) 연동도 활발합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gitleaks&quot;&gt;4. gitleaks&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 두 도구가 비밀을 &quot;안전하게 커밋&quot;하는 쪽이라면, &lt;a href=&quot;https://gitleaks.org/&quot;&gt;gitleaks&lt;/a&gt;는 반대로 &lt;strong&gt;비밀이 커밋되는 것을 찾아내고 막는&lt;/strong&gt; 오픈소스 스캐너입니다. Go로 작성된 단일 바이너리이며, 가장 널리 쓰이는 비밀 탐지 SAST(Static Application Security Testing) 도구 중 하나입니다(GitHub 스타 2.7만+). 최신 릴리스는 2026년 3월의 v8.30.1입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;무엇을_하나&quot;&gt;4.1. 무엇을 하나&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AWS 키, GitHub 토큰, Slack 웹훅 등 흔한 비밀 패턴에 대한 &lt;strong&gt;내장 룰&lt;/strong&gt;로 저장소를 스캔합니다. 정규식 + 엔트로피(무작위성) 검사를 조합합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;현재 파일뿐 아니라 &lt;strong&gt;git 히스토리 전체&lt;/strong&gt;를 훑습니다. 지금은 지운 비밀이라도 과거 커밋에 남아 있으면 잡아냅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;결과는 JSON, CSV, JUnit, SARIF 형식으로 출력할 수 있어 CI 연동이 쉽습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;기본_사용법&quot;&gt;4.2. 기본 사용법&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gitleaks git .            # 저장소의 커밋 히스토리 전체 스캔
gitleaks dir .            # 히스토리 없이 현재 디렉터리 파일만 스캔
gitleaks git . --pre-commit --staged   # 커밋 직전 스테이징된 변경만 검사&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;pre-commit 훅&lt;/strong&gt;으로 걸어두면 비밀이 히스토리에 들어가기 전에 차단합니다. 사후에 발견해서 히스토리를 세척하는 것보다 훨씬 쌉니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CI에는 공식 GitHub Action(&lt;code&gt;gitleaks/gitleaks-action&lt;/code&gt;)이 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;오탐은 &lt;code&gt;.gitleaks.toml`의 allowlist나 해당 줄의 `# gitleaks:allow&lt;/code&gt; 주석으로 무시합니다. v8.28.0부터는 주 룰과 보조 룰이 근접 거리 안에서 함께 매칭돼야 탐지로 치는 composite rules로 오탐을 줄일 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;알아둘_점_개발은_후속작으로&quot;&gt;4.3. 알아둘 점: 개발은 후속작으로&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메인테이너(Zach Rice)는 gitleaks를 기능 완성(feature-complete) 상태로 선언했고, 앞으로는 보안 패치만 나옵니다. 신규 개발은 같은 팀이 만드는 후속작 &lt;strong&gt;Betterleaks&lt;/strong&gt;로 옮겨갔습니다. Betterleaks는 CEL(Common Expression Language) 기반 룰 검증을 도입해 CredData 데이터셋 기준 98.6% 재현율을 내세웁니다. 다만 gitleaks도 여전히 사실상 표준으로 널리 쓰이고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;git_secrets와의_비교&quot;&gt;4.4. git-secrets와의 비교&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2절에서 언급한 AWS의 `git-secrets`와 목적이 같지만, gitleaks가 룰이 훨씬 풍부합니다(수백 개 내장 패턴). 히스토리 스캔, 리포트 형식, 유지보수 활성도 면에서도 앞서서, 요즘은 gitleaks(또는 trufflehog) 쪽이 표준에 가깝습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리_무엇을_쓸까&quot;&gt;5. 정리: 무엇을 쓸까&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;상황&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;권장&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;개인 로컬에서만 무시하고 싶은 파일&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;.git/info/exclude&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;팀 공통으로 무시할 파일&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;저장소에 비밀을 암호화해 커밋 (개인·소규모)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SOPS + age&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;저장소에 비밀을 커밋, 클라우드 IAM 연동&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SOPS + KMS&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;비밀을 아예 커밋하지 않기&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;플랫폼 환경 변수(Vercel 등), 시크릿 매니저&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;비밀이 실수로 커밋되는 것 방지&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;gitleaks(pre-commit 훅 + CI)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;6. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/getsops/sops&quot;&gt;getsops/sops (GitHub)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://getsops.io/docs/&quot;&gt;SOPS 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/getsops/sops/releases&quot;&gt;SOPS 릴리스&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.ansible.com/projects/ansible/latest/collections/community/sops/docsite/guide.html&quot;&gt;Ansible community.sops 가이드&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://gitleaks.org/&quot;&gt;Gitleaks 공식 사이트&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/gitleaks/gitleaks/releases&quot;&gt;gitleaks/gitleaks 릴리스&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/betterleaks-a-new-open-source-secrets-scanner-to-replace-gitleaks/&quot;&gt;Betterleaks 소개 (BleepingComputer)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>터미널 git diff를 GitHub처럼 보기: delta와 word-diff</title>
      <link>https://blog.benelog.net//git-diff-delta.html</link>
      <pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">git-diff-delta.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#delta_설치와_설정&quot;&gt;1. delta 설치와 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#상황별_diff_보기&quot;&gt;2. 상황별 diff 보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#zsh_scm_breeze에_붙이기&quot;&gt;3. zsh + scm_breeze에 붙이기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;4. 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;5. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GitHub UI에서 diff를 보면 바뀐 &lt;strong&gt;단어만&lt;/strong&gt; 진한 배경색으로 강조됩니다. 터미널의 `git diff`는 그렇지 않습니다. 한 글자만 고쳐도 줄 전체가 빨강/초록으로 칠해집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한글 문서를 쓰거나 교열할 때 이 차이가 특히 크게 느껴집니다. 문장 하나에서 조사만 바꿨는데 diff에서는 삭제 줄과 추가 줄이 통째로 나오니, 눈으로 두 줄을 대조해서 어디가 달라졌는지 찾아야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/git-diff-delta/img-00-plain.png&quot; alt=&quot;기본 git diff 화면&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;남긴다 → 기록한다&lt;/code&gt;, &lt;code&gt;덕분에 → 이 있어서&lt;/code&gt;, &lt;code&gt;방식 → 방법&lt;/code&gt;, &lt;code&gt;흔하게 → 흔히&lt;/code&gt;. 네 군데가 바뀌었는데 위 화면만 보고는 바로 알기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글에서는 이 문제를 해결한 설정을 공유합니다.
Ubuntu 24.04 + zsh + git 2.43 + scm_breeze 환경에서 확인했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;delta_설치와_설정&quot;&gt;1. delta 설치와 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dandavison/delta&quot;&gt;delta&lt;/a&gt;는 git의 pager를 대체하는 도구입니다. pager는 `git diff`나 `git log`의 긴 출력을 한 화면씩 넘겨 보여주는 프로그램으로, 기본값은 `less`입니다. delta는 이 자리를 대신하면서 구문 강조, 줄 번호, 그리고 &lt;strong&gt;줄 안에서 바뀐 부분만 강조하는 기능&lt;/strong&gt;을 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;설치&quot;&gt;1.1. 설치&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;apt에도 `git-delta`가 있지만 Ubuntu 24.04에 들어 있는 버전은 2023년 6월에 나온 0.16.5입니다. 최신 버전은 2026년 3월에 나온 0.19.2로 약 3년의 차이가 있습니다. sudo 없이 최신 바이너리를 받는 편이 낫습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;VER=0.19.2
curl -L -o /tmp/delta.tar.gz \
  https://github.com/dandavison/delta/releases/download/$VER/delta-$VER-x86_64-unknown-linux-gnu.tar.gz
tar xzf /tmp/delta.tar.gz -C /tmp
install -m755 /tmp/delta-$VER-x86_64-unknown-linux-gnu/delta ~/.local/bin/delta
delta --version&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;macOS는 &lt;code&gt;brew install git-delta&lt;/code&gt;, Arch는 `pacman -S git-delta`로 받으면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;gitconfig_설정&quot;&gt;1.2. ~/.gitconfig 설정&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.gitconfig&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;[core]
	pager = delta

[interactive]
	diffFilter = delta --color-only

[delta]
	navigate = true
	line-numbers = true
	hunk-header-decoration-style = blue box
	file-style = bold yellow
	file-decoration-style = yellow ul
	# 바뀐 부분만 배경색으로 강조 (GitHub UI와 같은 방식)
	minus-emph-style = normal &quot;#7a2323&quot;
	plus-emph-style = normal &quot;#2b5b2b&quot;
	# 한글은 음절, 영문·코드는 단어 단위로 토큰을 나눈다
	word-diff-regex = &quot;[가-힣]|[A-Za-z0-9_]+|[^ \t]&quot;
	max-line-distance = 1.0

[diff]
	colorMoved = default
	algorithm = histogram

[merge]
	conflictStyle = zdiff3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이제 평소처럼 `git diff`를 치면 이렇게 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/git-diff-delta/img-01-delta.png&quot; alt=&quot;delta 통합 뷰&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;바뀐 줄은 은은한 배경색, 그 줄에서 &lt;strong&gt;실제로 달라진 부분&lt;/strong&gt;은 진한 배경색으로 두 단계로 구분됩니다. GitHub의 diff 화면과 같은 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;word_diff_regex&quot;&gt;1.3. word-diff-regex&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 설정에서 가장 중요한 줄은 `word-diff-regex`입니다. 이 줄을 빼면 아래처럼 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/git-diff-delta/img-02-delta-default.png&quot; alt=&quot;delta 기본 토큰 단위 강조&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;delta의 기본값은 `\w+`인데, 유니코드 단어 문자 기준이라 한글 어절 하나가 통째로 토큰이 됩니다. 그래서 `저장하는 역할을 맡는다 → 저장한다`가 어절 세 개를 통째로 칠해 버립니다. 조사 하나만 고쳐도 앞 단어까지 같이 강조되니 정밀도가 떨어집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`[가-힣]`을 앞에 두면 한글은 음절 단위로 쪼개지고, `[A-Za-z0-9_]+`가 뒤에 있어서 영문 식별자와 클래스 이름은 통째로 유지됩니다. 결과적으로 &quot;저장&lt;strong&gt;하는 역할을 맡는&lt;/strong&gt;다 → 저장&lt;strong&gt;한&lt;/strong&gt;다&quot;처럼 바뀐 부분만 정확히 좁혀집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock caution&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caution&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 정규식은 delta에서만 통합니다. delta는 Rust regex(유니코드 인식)를 쓰지만, git 내장 `--word-diff-regex`는 POSIX ERE라 멀티바이트 문자 범위를 처리하지 못합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;$ git diff --word-diff-regex=&apos;[가-힣]|[A-Za-z0-9_]+&apos; ...
fatal: invalid regular expression: [가-힣]|[A-Za-z0-9_]+&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;git 쪽에서는 대신 &lt;code&gt;[^[:space:]]&lt;/code&gt;(공백이 아닌 모든 문자)를 쓰면 됩니다. 아래 인라인 word-diff 절에서 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;같이_켜면_좋은_설정&quot;&gt;1.4. 같이 켜면 좋은 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 `~/.gitconfig`에 끼워 둔 나머지 항목도 짧게 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;interactive.diffFilter = delta --color-only&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;`git add -p`에서도 같은 강조가 적용됩니다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;diff.colorMoved = default&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;내용은 그대로고 위치만 옮긴 블록을 별도 색으로 표시합니다. 절이나 함수 순서를 바꿀 때 유용합니다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;diff.algorithm = histogram&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;문단·함수를 옮겼을 때 diff 덩어리가 더 깔끔하게 묶입니다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;merge.conflictStyle = zdiff3&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;충돌 시 양쪽뿐 아니라 공통 조상 내용까지 보여줍니다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;delta.navigate = true&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;pager에서 &lt;code&gt;n&lt;/code&gt; / `N`으로 파일 사이를 건너뜁니다&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;상황별_diff_보기&quot;&gt;2. 상황별 diff 보기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;delta 통합 뷰가 기본이지만, 보는 목적에 따라 다른 방식이 더 편할 때가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;인라인_word_diff_한_줄에_겹쳐_보기&quot;&gt;2.1. 인라인 word-diff: 한 줄에 겹쳐 보기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;delta는 삭제 줄과 추가 줄을 위아래로 나란히 보여줍니다. 문장 교열처럼 &lt;strong&gt;한 줄 안의 변화만 확인&lt;/strong&gt;하고 싶을 때는 git 내장 `--word-diff`가 더 편합니다. 삭제와 추가가 한 줄에 겹쳐 나오므로 문장을 두 번 읽지 않아도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;git diff --word-diff=color --word-diff-regex=&apos;[^[:space:]]&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/git-diff-delta/img-03-worddiff.png&quot; alt=&quot;인라인 word-diff&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;남긴&lt;/code&gt;(빨강)&lt;code&gt;기록한&lt;/code&gt;(초록)`다`처럼 바로 이어져 나옵니다. `--word-diff-regex=&apos;[^[:space:]]&apos;`가 없으면 어절 전체가 잡히므로 이 옵션은 꼭 넣어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;delta는 &lt;code&gt;--word-diff&lt;/code&gt; 출력 형식을 이해하지 못해 색이 깨집니다. 그래서 이 명령은 pager를 `less`로 되돌려야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;좌우_2단_분할&quot;&gt;2.2. 좌우 2단 분할&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;넓은 화면에서 코드를 볼 때는 2단 분할이 낫습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;git -c delta.side-by-side=true diff&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/git-diff-delta/img-04-sbs.png&quot; alt=&quot;좌우 2단 분할&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기본값으로 켜지 않고 필요할 때만 켜는 편을 권합니다. 한글 문장은 한 줄이 길어서 폭이 반으로 줄면 줄바꿈이 많이 생깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;zsh_scm_breeze에_붙이기&quot;&gt;3. zsh + scm_breeze에 붙이기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/scmbreeze/scm_breeze&quot;&gt;scm_breeze&lt;/a&gt;를 쓰면 &lt;code&gt;gd&lt;/code&gt;(= &lt;code&gt;git diff&lt;/code&gt;), &lt;code&gt;gdc&lt;/code&gt;(= &lt;code&gt;git diff --cached&lt;/code&gt;) 같은 단축 alias가 이미 있습니다. 이들은 &lt;code&gt;git&lt;/code&gt; 함수를 거치므로 &lt;code&gt;core.pager = delta&lt;/code&gt; 설정을 그대로 탑니다. &lt;strong&gt;즉 `gd`는 아무것도 안 해도 바로 delta로 나옵니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문제는 scm_breeze가 기본 제공하는 &lt;code&gt;gdw&lt;/code&gt;(= &lt;code&gt;git diff --word-diff&lt;/code&gt;)입니다. delta를 타면서 색이 깨지고, 한글 정규식도 안 붙어 있습니다. 이것만 다시 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;~/.zshrc`의 scm_breeze `source&lt;/code&gt; 줄 &lt;strong&gt;뒤에&lt;/strong&gt; 넣습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.zshrc&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sh&quot;&gt;# delta 는 --word-diff 출력 형식을 이해하지 못해 색이 깨지므로
# word-diff 계열만 delta 를 우회하도록 다시 정의한다.
# scm_breeze 의 git 함수를 그대로 호출하므로 gs 가 매긴 파일 번호도 쓸 수 있다.
unalias gdw 2&amp;gt;/dev/null
_scmb_word_diff() {
    GIT_PAGER=&apos;less -R&apos; git diff --word-diff=color \
        --word-diff-regex=&apos;[^[:space:]]&apos; &quot;$@&quot;
}
gdw()  { _scmb_word_diff &quot;$@&quot;; }            # 단어(한글은 음절) 단위 인라인 diff
gdwc() { _scmb_word_diff --cached &quot;$@&quot;; }   # 스테이징된 변경만
gdws() { _scmb_word_diff -U0 &quot;$@&quot;; }        # 문맥 줄 없이 바뀐 곳만
gd2()  { git -c delta.side-by-side=true diff &quot;$@&quot;; }   # delta 좌우 2단 분할&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`unalias gdw`가 필요한 이유는, zsh에서 alias가 같은 이름의 함수보다 먼저 잡히기 때문입니다. `gds`를 쓰지 않고 `gd2`로 이름을 지은 것도 oh-my-zsh의 git 플러그인이 이미 `gds`를 쓰고 있어서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;scm_breeze를 안 쓴다면 `~/.gitconfig`에 alias로 넣어도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.gitconfig&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;[alias]
	wd = &quot;!git -c core.pager=less diff --word-diff=color --word-diff-regex=&apos;[^[:space:]]&apos; &quot;
	sd = &quot;!git -c delta.side-by-side=true diff &quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;4. 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 66.6667%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;명령&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;git diff&lt;/code&gt; (&lt;code&gt;gd&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;delta 통합 뷰. 바뀐 단어만 진한 배경색&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;gd2&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;delta 좌우 2단 분할&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;gdw&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;삭제·추가를 한 줄에 겹쳐 보는 인라인 word-diff&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;gdwc&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;스테이징된 변경만 word-diff&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;gdws&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;문맥 줄 없이 바뀐 곳만 (&lt;code&gt;-U0&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한글 문서를 다룬다면 두 가지만 기억하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;delta에는 &lt;code&gt;word-diff-regex = &quot;[가-힣]|[A-Za-z0-9_]+|[^ \t]&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;git 내장 word-diff에는 &lt;code&gt;--word-diff-regex=&apos;[^[:space:]]&apos;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기본값 그대로 쓰면 한글은 어절 통째로 강조돼서 GitHub만큼 정밀해지지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;5. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/dandavison/delta&quot;&gt;delta: A syntax-highlighting pager for git, diff, grep, and blame output&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://dandavison.github.io/delta/&quot;&gt;delta 공식 매뉴얼&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---word-diffltmodegt&quot;&gt;git diff --word-diff 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/scmbreeze/scm_breeze&quot;&gt;scm_breeze&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Hugging Face 오픈 모델의 호환성을 만드는 사실상의 표준들</title>
      <link>https://blog.benelog.net//huggingface-open-model-standards.html</link>
      <pubDate>Sun, 7 Jun 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">huggingface-open-model-standards.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#모델_가중치_저장_포맷&quot;&gt;1. 모델 가중치 저장 포맷&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#모델_아키텍처_규약_transformers_라이브러리의_역할&quot;&gt;2. 모델 아키텍처 규약: transformers 라이브러리의 역할&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#추론_엔진_간_호환성&quot;&gt;3. 추론 엔진 간 호환성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#앞_글의_레이어_분류와_겹쳐_보기&quot;&gt;4. 앞 글의 레이어 분류와 겹쳐 보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#핵심_정리&quot;&gt;5. 핵심 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;6. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;llm-information-layers.html&quot;&gt;모델 레이어를 다룬 글&lt;/a&gt;에서 모델이 &quot;아는 것&quot;은 가중치(weight)라는 숫자값에 압축돼 있다고 정리했습니다. 그 가중치는 결국 파일로 저장되어 Hugging Face에서 공유됩니다. Hugging Face는 오픈 모델과 데이터셋, 데모를 올리고 내려받는 중심 플랫폼으로, 흔히 &quot;머신러닝계의 GitHub&quot;에 비유됩니다. 그런데 이렇게 공유된 모델은 누가 어떤 환경에서 학습했든, 다운로드해서 바로 실행할 수 있는 경우가 많습니다. 이 글은 그 호환성의 토대가 되는 사실상의 표준(de facto standard)들을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;크게 &lt;strong&gt;모델 가중치 포맷&lt;/strong&gt;, &lt;strong&gt;모델 아키텍처 규약&lt;/strong&gt;, &lt;strong&gt;메타데이터/설정 표준&lt;/strong&gt; 세 층위로 나눠 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;모델_가중치_저장_포맷&quot;&gt;1. 모델 가중치 저장 포맷&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;포맷&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;Safetensors&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;현재 사실상 표준. 메모리 매핑&lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_1&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_1&quot; title=&quot;View footnote.&quot;&gt;1&lt;/a&gt;]&lt;/sup&gt;이 가능하고, pickle(Python 기본 직렬화 방식)을 쓰지 않아 역직렬화 시 임의 코드가 실행되는 보안 취약점이 없습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;GGUF&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;llama.cpp 생태계의 표준. 양자화&lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_2&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_2&quot; title=&quot;View footnote.&quot;&gt;2&lt;/a&gt;]&lt;/sup&gt; 정보를 파일 자체에 포함하며, CPU·엣지를 비롯한 다양한 환경의 추론에 강합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;PyTorch (&lt;code&gt;.bin&lt;/code&gt;/&lt;code&gt;.pt&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;pickle 기반의 레거시 포맷. 여전히 많이 쓰이지만 역직렬화 과정에 보안 우려가 있습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;신경망의 가중치는 &lt;strong&gt;텐서(tensor)&lt;/strong&gt;, 즉 숫자가 여러 차원으로 늘어선 배열에 담깁니다. 스칼라(0차원)·벡터(1차원)·행렬(2차원)을 임의 차원으로 일반화한 것으로, 각 레이어의 가중치 하나하나가 이런 텐서입니다. 텐서마다 배열의 형태(shape, 예: 4096 × 4096)와 각 숫자의 자료형(dtype, 예: &lt;code&gt;float16&lt;/code&gt;·&lt;code&gt;bfloat16&lt;/code&gt;)이 정해져 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Safetensors&lt;/strong&gt;는 이 텐서의 이름(key)과 shape, dtype, 바이너리 데이터를 단순한 헤더+바이너리 구조로 저장합니다. 그래서 어떤 프레임워크든 쉽게 읽을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다국어 지원 능력도 이 가중치 안에 들어 있습니다. 학습 시 Common Crawl, Wikipedia 등에서 수집한 수십~수백 개 언어의 텍스트 패턴이 신경망 파라미터 전체에 걸쳐 &lt;strong&gt;분산 표현(distributed representation)&lt;/strong&gt; 으로 인코딩됩니다. &quot;한국어 사전&quot; 같은 별도 파일이 있는 게 아니라, embedding layer와 attention/FFN layer의 수치 값들에 각 언어의 문법·어휘·의미가 녹아들어 있습니다. 그래서 특정 언어만 빼거나 넣기가 쉽지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;모델_아키텍처_규약_transformers_라이브러리의_역할&quot;&gt;2. 모델 아키텍처 규약: transformers 라이브러리의 역할&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hugging Face `transformers`는 수많은 모델 아키텍처를 통일된 인터페이스로 불러오고 실행하게 해주는 오픈소스 Python 라이브러리입니다. Transformer 아키텍처에서 이름을 따왔지만 지금은 그 밖의 구조도 폭넓게 지원하며, 모델을 불러오는 방식의 사실상 표준 역할을 합니다. 모델을 어떻게 읽고 실행할지는 가중치 파일 옆에 놓인 설정 파일들이 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;config.json&lt;/code&gt;&lt;/strong&gt; — 모델 구조 정의 (&lt;code&gt;model_type&lt;/code&gt;, &lt;code&gt;hidden_size&lt;/code&gt;, &lt;code&gt;num_attention_heads&lt;/code&gt; 등)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tokenizer.json&lt;/code&gt; / &lt;code&gt;tokenizer_config.json&lt;/code&gt;&lt;/strong&gt; — 토크나이저 정의. BPE(Byte Pair Encoding) 같은 알고리즘으로 다국어 텍스트를 토큰으로 분리하는 토큰 사전(vocabulary, 보통 3만~25만 토큰)을 담습니다. 한국어 &quot;안녕하세요&quot;도, 영어 &quot;hello&quot;도 이 사전의 토큰 조합으로 표현됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;generation_config.json&lt;/code&gt;&lt;/strong&gt; — 생성 파라미터&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;config.json 예시&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;model_type&quot;: &quot;llama&quot;,
  &quot;hidden_size&quot;: 4096,
  &quot;num_attention_heads&quot;: 32,
  &quot;num_hidden_layers&quot;: 32,
  &quot;vocab_size&quot;: 32000
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;model_type`이 `&quot;llama&quot;`이면, 라이브러리가 `LlamaForCausalLM&lt;/code&gt; 클래스를 자동으로 선택해 가중치를 로드합니다. 그래서 Llama 아키텍처 기반 모델은 누가 학습했든 동일한 코드로 실행됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;추론_엔진_간_호환성&quot;&gt;3. 추론 엔진 간 호환성&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 Hugging Face 모델 하나(safetensors + config.json)를 여러 추론 엔진(학습된 모델을 받아 실제로 실행하고 응답을 생성하는 런타임)이 받아 실행할 수 있습니다. 일부는 파일을 그대로 읽고, 일부는 포맷 변환을 거칩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;transformers&lt;/strong&gt;: Hugging Face 표준 Python 라이브러리&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;vLLM&lt;/strong&gt;: 고성능 서빙에 특화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TGI(Text Generation Inference)&lt;/strong&gt;: Hugging Face 자체 서빙 엔진&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ollama / llama.cpp&lt;/strong&gt;: GGUF로 변환해 CPU/엣지에서 실행&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ONNX Runtime&lt;/strong&gt;: ONNX(Open Neural Network Exchange) 포맷으로 변환해 실행&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이것이 가능한 이유는 &lt;strong&gt;아키텍처가 표준화&lt;/strong&gt;되어 있기 때문입니다. 대부분의 오픈 모델이 소수의 아키텍처(Llama, Mistral, Qwen, Gemma 등)를 따르고, 각 추론 엔진이 이 아키텍처들을 미리 구현해 둡니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;앞_글의_레이어_분류와_겹쳐_보기&quot;&gt;4. 앞 글의 레이어 분류와 겹쳐 보기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;llm-information-layers.html&quot;&gt;앞 글&lt;/a&gt;에서는 LLM에 정보가 들어가는 자리를 모델 레이어와 프롬프트 레이어로 나누고, 아키텍처·디코딩 같은 축을 덧붙였습니다. Hugging Face 모델 저장소의 파일 구성을 그 분류에 겹쳐 보면 거의 일대일로 대응합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;파일&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;대응하는 레이어&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;담는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;model.safetensors&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모델 레이어&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;학습으로 결정된 가중치(텐서) 그 자체&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;config.json&lt;/code&gt; (&lt;code&gt;model_type&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;아키텍처 레이어&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;어떤 형태로 정보를 담을 것인가&quot; — 레이어 수, 헤드 수, 아키텍처 종류&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;tokenizer.json&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;프롬프트 레이어 (토큰화)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;컨텍스트로 들어갈 텍스트를 토큰으로 쪼개는 규칙&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;generation_config.json&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;디코딩/샘플링 레이어&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;temperature, top-p 같은 출력 제어 기본값&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;바꾸려는 대상에 따라 손대는 파일이 갈립니다. 지식 자체를 바꾸려면 `model.safetensors`를, 샘플링이나 출력 길이 같은 생성 기본값만 바꾸려면 `generation_config.json`을 건드립니다. 앞 글이 &quot;정보가 어디에 저장되는가&quot;의 지도였다면, 이 파일 구성은 그 지도가 디스크에 실제로 어떻게 놓이는지를 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;핵심_정리&quot;&gt;5. 핵심 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;공식적인 &quot;표준 기구&quot;가 정한 스펙이라기보다는, 다음 세 가지가 맞물린 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Safetensors&lt;/strong&gt; — 텐서를 저장하는 단순하고 안전한 바이너리 포맷&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;transformers의 config 규약&lt;/strong&gt; — `config.json`의 `model_type`으로 아키텍처를 식별&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;소수 아키텍처로의 수렴&lt;/strong&gt; — 대부분의 모델이 Llama/Mistral 계열 아키텍처를 채택&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 세 가지가 맞물려 &quot;누가 학습했든 다운받으면 바로 돌릴 수 있는&quot; 생태계가 만들어졌습니다. ISO 같은 공식 표준이 아니라 사실상의 표준(de facto standard)입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;6. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/huggingface/safetensors&quot;&gt;Safetensors (GitHub)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://huggingface.co/docs/transformers/index&quot;&gt;Hugging Face Transformers 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://huggingface.co/docs/transformers/model_doc/auto&quot;&gt;Auto Classes — &lt;code&gt;model_type&lt;/code&gt; 기반 자동 로딩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://huggingface.co/docs/hub/gguf&quot;&gt;GGUF — Hugging Face Hub 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;footnotes&quot;&gt;
&lt;hr&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_1&quot;&gt;
&lt;a href=&quot;#_footnoteref_1&quot;&gt;1&lt;/a&gt;. 파일을 통째로 읽어 들이지 않고, 디스크의 내용을 필요한 부분만 메모리 주소에 바로 연결해 다루는 방식. 큰 모델도 빠르게 로드할 수 있습니다.
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_2&quot;&gt;
&lt;a href=&quot;#_footnoteref_2&quot;&gt;2&lt;/a&gt;. 가중치를 더 적은 비트로 표현해(예: 16비트를 4비트로) 모델 용량과 메모리 사용량을 줄이는 기법. 약간의 정확도를 내주고 크기를 줄입니다.
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>대량 데이터를 입출력하는 DBMS별 명령어와 도구(MySQL, Oracle, SQL Server, PostgreSQL)</title>
      <link>https://blog.benelog.net//dbms-bulk-data-handling.html</link>
      <pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">dbms-bulk-data-handling.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#mysql&quot;&gt;1. MySQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#oracle&quot;&gt;2. Oracle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ms_sql_server&quot;&gt;3. MS SQL Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#postgresql&quot;&gt;4. PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DBMS별로 최적화된 대량 데이터 로딩과 추출 기능을 전용 SQL 구문이나 도구로 제공합니다. 이 방식들은 구현 원리상 자바 애플리케이션으로는 도달하기 어려운 성능을 발휘합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;mysql&quot;&gt;1. MySQL&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;MySQL에서는 &lt;code&gt;SELECT &amp;#8230;&amp;#8203; INTO OUTFILE&lt;/code&gt; 명령으로 데이터를 익스포트하고, &lt;code&gt;LOAD DATA INFILE&lt;/code&gt; 명령으로 임포트합니다. &lt;code&gt;SELECT &amp;#8230;&amp;#8203; INTO OUTFILE&lt;/code&gt; 은 다음과 같은 형식으로 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;SELECT [칼럼명]
INTO OUTFILE [파일명]
FROM [테이블명]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;FIELDS&lt;/code&gt; 와 &lt;code&gt;LINES&lt;/code&gt; 절을 추가하면 필드와 행 사이를 구분하는 문자를 지정할 수 있습니다. 디폴트로는 탭 문자(&lt;code&gt;\t&lt;/code&gt;)로 칼럼 사이를, 새 줄 문자(&lt;code&gt;\n&lt;/code&gt;)로 행 사이를 구분합니다. &lt;code&gt;baseball.players&lt;/code&gt; 테이블의 내용을 &lt;code&gt;players.csv&lt;/code&gt; 라는 파일명으로 출력하는 예시는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;SELECT back_num, name, position
INTO OUTFILE &apos;players.csv&apos;
FROM baseball.players
FIELDS TERMINATED BY &apos;,&apos; OPTIONALLY ENCLOSED BY &apos;&quot;&apos;
LINES TERMINATED BY &apos;\n&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;LOAD DATA INFILE&lt;/code&gt; 명령어는 아래와 같은 형식으로 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;LOAD DATA INFILE [파일명] INTO TABLE [테이블명]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마찬가지로 필드와 라인 구분자를 지정할 수 있고, &lt;code&gt;SELECT &amp;#8230;&amp;#8203; INTO OUTFILE&lt;/code&gt; 과 같은 방식으로 옵션을 지정합니다. &lt;code&gt;players.csv&lt;/code&gt; 의 내용을 &lt;code&gt;baseball.players&lt;/code&gt; 테이블로 로딩하는 명령어는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;LOAD DATA INFILE &apos;players.csv&apos;
INTO TABLE baseball.players
FIELDS TERMINATED BY &apos;,&apos; OPTIONALLY ENCLOSED BY &apos;&quot;&apos;
LINES TERMINATED BY &apos;\n&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대량 적재의 성능은 스토리지 엔진에 따라 손볼 곳이 다릅니다. MySQL 5.5부터 기본 스토리지 엔진이 InnoDB이고 요즘은 대부분 InnoDB를 쓰므로, InnoDB 기준으로 정리합니다. InnoDB 테이블에 대량으로 적재할 때는 &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; 를 충분히 확보하고, 적재 구간에서 자동 커밋과 무결성 검사를 잠시 꺼 부가 작업을 줄이는 방식이 효과적입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;SET autocommit = 0;
SET unique_checks = 0;
SET foreign_key_checks = 0;

LOAD DATA INFILE &apos;players.csv&apos;
INTO TABLE baseball.players
FIELDS TERMINATED BY &apos;,&apos; OPTIONALLY ENCLOSED BY &apos;&quot;&apos;
LINES TERMINATED BY &apos;\n&apos;;

COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;적재가 끝나면 꺼 두었던 &lt;code&gt;unique_checks&lt;/code&gt; 와 &lt;code&gt;foreign_key_checks&lt;/code&gt; 는 다시 켜야 합니다. 참고로 지금은 거의 쓰이지 않는 MyISAM 엔진에서는 &lt;code&gt;bulk_insert_buffer_size&lt;/code&gt;, &lt;code&gt;myisam_sort_buffer_size&lt;/code&gt; 같은 변수가 대량 삽입 성능에 영향을 줍니다. 다만 이 값들은 InnoDB 테이블에는 적용되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;LOAD DATA INFILE&lt;/code&gt; 명령은 &lt;code&gt;mysqlimport&lt;/code&gt; 라는 도구로도 수행할 수 있습니다. 이 도구를 쓸 때 네트워크를 통해 파일이 전송된다면 데이터를 압축해 성능 향상을 꾀할 수 있습니다. 예전에는 &lt;code&gt;--compress&lt;/code&gt; 옵션을 썼으나, MySQL 8.0.18부터는 이 옵션이 deprecated되어 &lt;code&gt;--compression-algorithms&lt;/code&gt; 로 압축 알고리즘을 지정하는 방식으로 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한 가지 주의할 점은 최근 MySQL이 보안상의 이유로 파일 입출력에 제약을 둔다는 것입니다. &lt;code&gt;SELECT &amp;#8230;&amp;#8203; INTO OUTFILE&lt;/code&gt; 과 (&lt;code&gt;LOCAL&lt;/code&gt; 을 붙이지 않은) &lt;code&gt;LOAD DATA INFILE&lt;/code&gt; 은 서버의 &lt;code&gt;secure_file_priv&lt;/code&gt; 시스템 변수가 가리키는 디렉터리 안에서만 동작하며, 이 변수는 MySQL 5.7.6부터 기본값이 설정되어 있습니다. 또한 클라이언트 쪽 파일을 읽는 &lt;code&gt;LOAD DATA LOCAL INFILE&lt;/code&gt; 은 MySQL 8.0부터 &lt;code&gt;local_infile&lt;/code&gt; 변수가 기본적으로 꺼져 있어, 서버와 클라이언트 양쪽에서 이를 명시적으로 켜야 동작합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자세한 내용은 아래 페이지를 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;SELECT &amp;#8230;&amp;#8203; INTO OUTFILE&lt;/code&gt; : &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/select-into.html&quot;&gt;MySQL Reference Manual&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LOAD DATA&lt;/code&gt; : &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/load-data.html&quot;&gt;MySQL Reference Manual&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mysqlimport&lt;/code&gt; : &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/mysqlimport.html&quot;&gt;MySQL Reference Manual&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;LOAD DATA LOCAL INFILE&lt;/code&gt; 보안 고려 사항 : &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/load-data-local-security.html&quot;&gt;MySQL Reference Manual&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;oracle&quot;&gt;2. Oracle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Oracle에서는 &lt;code&gt;SQL*Loader&lt;/code&gt;, Data Pump API 같은 도구나 External Table을 생성해서 데이터 로딩을 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SQL*Loader에서 다루는 파일들은 역할이 상세하게 구분됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;컨트롤 파일 : 수행할 명령문을 담은 파일.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;입력 데이터 파일 : DB로 임포트할 데이터를 담은 파일. 컨트롤 파일에서 &lt;code&gt;INFILE *&lt;/code&gt; 구문을 사용하면 컨트롤 파일에 데이터를 포함시킬 수도 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;로그 파일 : 작업 과정과 결과를 기록하는 파일.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bad 파일 : 입력이 거부된 행들이 저장되는 파일. 예를 들면 형식이 틀려서 SQL*Loader가 해석할 수 없거나 DB의 제약조건을 충족시키지 못한 행들이 이 파일로 기록됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Discard 파일 : 컨트롤 파일에서 정한 규칙에 의해 필터링되는 행들이 기록되는 파일.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컨트롤 파일은 아래와 같이 명령행에서 위치를 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sqlldr userid=scott/tiger control=&apos;./control.ctl&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컨트롤 파일 안에서는 아래와 같이 입력할 데이터 파일의 위치와 형식, 값이 들어갈 테이블을 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;control.ctl&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;LOAD DATA
INFILE &apos;players.csv&apos;
INTO players
FIELDS TERMINATED BY &apos;,&apos; OPTIONALLY ENCLOSED BY &apos;&quot;&apos;
( back_num, name, position )&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;성능을 높이기 위해 아래 옵션값을 조정해볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;direct&lt;/code&gt; : &lt;code&gt;Direct Path Load&lt;/code&gt; 를 사용할지를 true/false로 지정합니다. 이 방식을 쓰면 내부적으로 SQL 명령을 생성하지 않고 데이터베이스 블록에 직접 쓰기 작업을 하기 때문에 성능이 더 좋아집니다. &lt;code&gt;direct&lt;/code&gt; 옵션의 디폴트는 false로, 특별히 지정하지 않으면 데이터 로딩을 INSERT 명령으로 수행하는 &lt;code&gt;Conventional Path Load&lt;/code&gt; 가 활성화됩니다. &lt;code&gt;Direct Path Load&lt;/code&gt; 를 쓸 때는 데이터 로딩 도중에 인덱스 업데이트와 제약조건 등이 바로 적용되지 않으며, 로딩 작업의 시작과 끝에 테이블과 인덱스에 락을 잡습니다. 따라서 로딩 작업 도중에 다른 프로세스에서도 대상 테이블에 접근해야 한다면 적합하지 않습니다. 클러스터링된 테이블에도 사용할 수 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parallel&lt;/code&gt; : 병렬 수행 여부입니다. parallel 모드로 데이터를 올리는 중에는 인덱스가 업데이트되지 않으므로, 데이터 업로드가 끝난 후에 인덱스를 재생성해줘야 합니다. &lt;code&gt;direct=true&lt;/code&gt; 와 함께 쓰려면 대상 테이블에 트리거나 제약조건이 비활성화되어 있어야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rows&lt;/code&gt; : 한 번에 운반하는 행의 개수입니다. &lt;code&gt;direct=false&lt;/code&gt; 일 때는 commit이 일어나는 단위로, 스프링 배치의 &lt;code&gt;commit-interval&lt;/code&gt; 과 유사합니다. &lt;code&gt;direct=true&lt;/code&gt; 로 &lt;code&gt;Direct Path Load&lt;/code&gt; 를 쓸 때도 rows가 데이터를 저장하는 단위라는 점은 비슷하지만, 이 모드에서는 중간에 인덱스가 갱신되지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 외에도 direct 옵션을 사용할 때는 &lt;code&gt;columnarrayrows&lt;/code&gt;, &lt;code&gt;streamsize&lt;/code&gt;, 아닐 때는 &lt;code&gt;bindsize&lt;/code&gt; 등의 옵션으로 데이터의 운반 단위를 조정할 수 있습니다. 위의 옵션은 명령행에서 지정할 수 있습니다. 예를 들어 direct와 parallel 모드를 동시에 사용해서 데이터를 임포트하려면 아래와 같이 실행합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sqlldr userid=scott/tiger control=&apos;./control1.ctl&apos; DIRECT=TRUE PARALLEL=true
sqlldr userid=scott/tiger control=&apos;./control2.ctl&apos; DIRECT=TRUE PARALLEL=true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Oracle 9i부터는 데이터를 테이블스페이스 바깥의 파일로 유지하는 External Table이 제공됩니다. CSV 형식의 외부 파일을 External Table로 연결하고 SELECT 구문으로 조회해서 테이블에 저장하는 작업도 가능합니다. External Table에 SELECT를 할 때 JOIN, SORT도 사용할 수 있고, 읽기 작업을 병렬로 수행할 수 있다는 장점도 있습니다. 그러나 읽기 전용 작업만 허용되기 때문에 DELETE나 UPDATE 같은 SQL을 External Table을 대상으로 수행할 수는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 Oracle DB 간의 테이블 단위 데이터 로딩에는 import, export 유틸리티가 더 성능에 유리합니다. Oracle Database 10.1(10g release 1) 이후부터는 이를 대체하는 Data Pump API가 제공됩니다. 자세한 내용은 아래를 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SQL*Loader : &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/sutil/oracle-sql-loader-concepts.html&quot;&gt;Oracle Database Utilities&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;External Table : &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/sutil/oracle-external-tables-concepts.html&quot;&gt;Oracle Database Utilities&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Data Pump API : &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/sutil/oracle-data-pump-overview.html&quot;&gt;Oracle Database Utilities&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ms_sql_server&quot;&gt;3. MS SQL Server&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SQL Server에서는 OPENROWSET과 BULK INSERT 구문이 제공되고, bcp나 SSIS(Integration Services) 패키지 같은 별도의 도구로도 비슷한 작업을 수행할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BULK INSERT 구문은 파일을 지정하는 것 외에는 보통의 INSERT 문처럼 사용할 수 있습니다. &lt;code&gt;players.csv&lt;/code&gt; 에서 players 테이블로 데이터를 입력하는 예제는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;BULK INSERT players
FROM &apos;players.csv&apos;
WITH (
  FIELDTERMINATOR = &apos;,&apos;,
  ROWTERMINATOR = &apos;\n&apos;
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BULK INSERT를 할 때는 아래와 같은 옵션을 조정해 성능을 높일 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tablock&lt;/code&gt; : 대량 입력 작업 동안 테이블을 잠급니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rows_per_batch&lt;/code&gt; : 하나의 트랜잭션으로 처리할 행의 개수입니다. 스프링 배치의 &lt;code&gt;commit-interval&lt;/code&gt; 속성과 유사합니다. 되도록 큰 값을 지정하는 것이 성능 향상에 유리하나, 인덱스가 있는 테이블에서 지나치게 큰 값을 지정하면 메모리 사용량이 많아지고 병렬 업로드 시 테이블 락을 유발할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞선 예제에서 테이블 락을 걸고 3000개씩 commit을 하는 옵션을 추가하면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;BULK INSERT players
FROM &apos;players.csv&apos;
WITH (
  FIELDTERMINATOR = &apos;,&apos;,
  ROWTERMINATOR = &apos;\n&apos;,
  TABLOCK,
  ROWS_PER_BATCH = 3000
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파일로부터 데이터를 읽어 들이는 또 하나의 방법인 OPENROWSET 구문은 아래와 같이 SELECT 문장과 함께 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;SELECT *
FROM OPENROWSET(&apos;MSDASQL&apos;,
     &apos;Driver={Microsoft Text Driver (*.txt; *.csv)};DefaultDir=C:\data;&apos;,
     &apos;SELECT * FROM players.csv&apos;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;칼럼명은 텍스트 파일의 첫 줄에 포함시키거나 Schema.ini 파일로 정의할 수 있습니다. 다만 위 예제에서 쓰인 &lt;code&gt;Microsoft Text Driver&lt;/code&gt; 는 32비트 전용인 구형 Jet 기반 드라이버로, 현재는 권장되지 않습니다. 최근 환경에서는 64비트를 지원하는 ACE(Access Connectivity Engine) 공급자(&lt;code&gt;Microsoft.ACE.OLEDB.x&lt;/code&gt;)를 쓰거나, 아예 SQL Server가 직접 파일을 읽는 &lt;code&gt;OPENROWSET(BULK &amp;#8230;&amp;#8203;)&lt;/code&gt; 구문을 사용하는 편이 낫습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SQL Server는 bcp라는 별도의 데이터 임포트/익스포트 유틸리티도 제공합니다. bcp는 본래 Sybase에서 유래한 도구로, SAP ASE(과거 Sybase ASE)의 bcp와도 사용법이 비슷합니다. 데이터 익스포트와 임포트를 모두 SQL Server를 대상으로 할 때는 네이티브 형식을 이용하면 성능이 더 향상됩니다. BULK INSERT 구문에서는 &lt;code&gt;DATAFILETYPE = &apos;native&apos;&lt;/code&gt; 옵션으로, bcp에서는 &lt;code&gt;-n&lt;/code&gt; 옵션을 지정해서 네이티브 파일을 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 SQL Server와 함께 제공되는 데이터 통합 도구인 SSIS(Integration Services) 패키지로도 다양한 자원에서 데이터를 입출력할 수 있습니다. GUI 도구에서 작업을 설계하고 dtexec.exe 라는 실행 파일로 커맨드라인에서 작업을 실행하는 방식도 가능합니다. ETL 도구와 유사한 접근법입니다. 앞에서 BULK INSERT 문의 옵션이었던 tablock, rows_per_batch 등의 값도 SSIS에서 설정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각각의 방식에 대한 자세한 설명은 아래 페이지를 참고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;BULK INSERT : &lt;a href=&quot;https://learn.microsoft.com/ko-kr/sql/t-sql/statements/bulk-insert-transact-sql&quot;&gt;Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OPENROWSET : &lt;a href=&quot;https://learn.microsoft.com/ko-kr/sql/t-sql/functions/openrowset-transact-sql&quot;&gt;Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;bcp : &lt;a href=&quot;https://learn.microsoft.com/ko-kr/sql/tools/bcp-utility&quot;&gt;Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BULK INSERT/OPENROWSET으로 데이터 가져오기 : &lt;a href=&quot;https://learn.microsoft.com/ko-kr/sql/relational-databases/import-export/import-bulk-data-by-using-bulk-insert-or-openrowset-bulk-sql-server&quot;&gt;Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SSIS : &lt;a href=&quot;https://learn.microsoft.com/ko-kr/sql/integration-services/sql-server-integration-services&quot;&gt;Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;postgresql&quot;&gt;4. PostgreSQL&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;PostgreSQL에서는 COPY 구문, Foreign Data Wrapper, pg_bulkload를 활용해 텍스트 파일을 바로 DB에 익스포트/임포트할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테이블에 있는 데이터를 COPY 구문으로 아래와 같이 익스포트합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;COPY (SELECT name, position, back_num FROM players)
TO &apos;players.csv&apos;
WITH (FORMAT CSV);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;임포트 명령은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;COPY players (name, position, back_num)
FROM &apos;/data/players.csv&apos; DELIMITER &apos;,&apos; CSV;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마찬가지로 같은 PostgreSQL끼리 데이터를 주고받을 때는 포맷을 &lt;code&gt;BINARY&lt;/code&gt; 로 지정하면 성능 향상을 유도할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;PostgreSQL에서는 DB 밖에 저장된 데이터를 DB 안의 객체처럼 접근하는 Foreign Data Wrapper라는 개념도 지원합니다. Oracle의 External Table이나 SQL Server의 OPENROWSET 방식과 유사합니다. 아래와 같이 CREATE EXTENSION, CREATE SERVER 구문으로 file_fdw 모듈을 설치합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;CREATE EXTENSION file_fdw;
CREATE SERVER baseball FOREIGN DATA WRAPPER file_fdw;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 파일에서 읽어올 데이터의 스키마를 아래와 같이 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;CREATE FOREIGN TABLE players (
  name VARCHAR(30),
  position VARCHAR(30),
  back_num INTEGER
) SERVER baseball
OPTIONS ( filename &apos;/home/benelog/players.csv&apos;, format &apos;csv&apos; );&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;format, header, delimiter 등의 옵션은 COPY 구문과 똑같이 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;pg_bulkload라는 전용 도구도 지원합니다. Oracle의 SQL*Loader처럼 ctl 파일에 작업의 명세를 정의할 수 있습니다. CSV 파일을 로딩하는 ctl 파일은 아래와 같이 정의합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;players_csv.ctl&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;properties&quot;&gt;OUTPUT = players
INPUT = /home/benelog/players.csv
TYPE = CSV
QUOTE = &quot;\&quot;&quot;
ESCAPE = \
DELIMITER = &quot;,&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;명령행에서 다음과 같이 실행하면 CSV 파일의 내용이 테이블로 입력됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;pg_bulkload players_csv.ctl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각각의 방식에 대한 자세한 사용법은 다음을 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;COPY 구문 : &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-copy.html&quot;&gt;PostgreSQL Documentation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Foreign Data Wrapper : &lt;a href=&quot;https://wiki.postgresql.org/wiki/Foreign_data_wrappers&quot;&gt;PostgreSQL Wiki&lt;/a&gt; , &lt;a href=&quot;https://www.postgresql.org/docs/current/file-fdw.html&quot;&gt;file_fdw 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pg_bulkload : &lt;a href=&quot;https://github.com/ossc-db/pg_bulkload&quot;&gt;GitHub 저장소&lt;/a&gt; (문서: &lt;a href=&quot;https://ossc-db.github.io/pg_bulkload/&quot;&gt;pg_bulkload Documentation&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>RAG 시스템의 구성요소와 성능을 좌우하는 요소</title>
      <link>https://blog.benelog.net//rag-system-components.html</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">rag-system-components.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#rag_파이프라인의_기본_흐름&quot;&gt;1. RAG 파이프라인의 기본 흐름&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#vector_db는_어떤_역할을_하는가&quot;&gt;2. Vector DB는 어떤 역할을 하는가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#한국어_처리에서_중요한_구성요소&quot;&gt;3. 한국어 처리에서 중요한 구성요소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rag_안에서_쓰이는_llm들&quot;&gt;4. RAG 안에서 쓰이는 LLM들&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#성능이_안_나올_때_어디를_손볼_것인가&quot;&gt;5. 성능이 안 나올 때 어디를 손볼 것인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#어디부터_손댈지&quot;&gt;6. 어디부터 손댈지&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;llm-information-layers.html&quot;&gt;앞 글&lt;/a&gt;에서 RAG가 결국 프롬프트 레이어에 데이터를 공급하는 장치지만, 앞단의 검색/필터링 파이프라인이 충분히 복잡해 별도 서브시스템으로 다룰 만하다고 정리했습니다. 이 글은 그 서브시스템 안을 들여다봅니다. 어떤 부품으로 구성되는지, 한국어를 다룰 때 어디가 까다로운지, 성능이 안 나올 때 어디를 의심해야 하는지를 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;rag_파이프라인의_기본_흐름&quot;&gt;1. RAG 파이프라인의 기본 흐름&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전형적인 RAG는 두 시점으로 나눠 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;dlist&quot;&gt;
&lt;dl&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;&lt;strong&gt;인덱싱 시점 (오프라인)&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;원본 문서 → 청킹 → 임베딩 → Vector DB 저장. 메타데이터(출처, 작성일, 권한 등)도 함께 저장.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt class=&quot;hdlist1&quot;&gt;&lt;strong&gt;질의 시점 (온라인)&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;
&lt;p&gt;사용자 질문 → 쿼리 재작성 → 임베딩 → Vector DB 검색 (+ 키워드 검색) → 재순위(rerank) → 컨텍스트 조립 → LLM에 프롬프트로 전달 → 응답 생성.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 단계마다 별도 모델/시스템이 끼어들 수 있고, 단계마다 다른 튜닝이 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;vector_db는_어떤_역할을_하는가&quot;&gt;2. Vector DB는 어떤 역할을 하는가&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Vector DB는 한마디로 &lt;strong&gt;&quot;고차원 벡터의 근사 최근접 이웃(approximate nearest neighbor, ANN) 검색에 특화된 저장소&quot;&lt;/strong&gt; 입니다. 임베딩 모델이 만든 수백~수천 차원 벡터를 저장하고, &quot;쿼리 벡터와 가장 가까운 K개&quot;를 빠르게 돌려줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;핵심 책임은 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;벡터 인덱싱&lt;/strong&gt;: HNSW, IVF, PQ 같은 ANN 인덱스로 검색을 sub-linear 시간에 처리합니다. 정확도 vs. 속도/메모리의 트레이드오프가 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;유사도 계산&lt;/strong&gt;: 코사인 유사도, 내적, L2 거리 등. 임베딩 모델이 가정한 거리에 맞춰야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;메타데이터 필터링&lt;/strong&gt;: &quot;2025년 이후 문서만&quot;, &quot;권한이 있는 문서만&quot; 같은 조건을 벡터 검색과 결합. &lt;strong&gt;pre-filter / post-filter&lt;/strong&gt; 시점이 성능을 크게 좌우합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;흔한 오해 지점들이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vector DB가 의미를 이해하지는 않습니다.&lt;/strong&gt; 의미를 벡터로 인코딩하는 건 임베딩 모델이 하고, Vector DB는 벡터 간 기하학적 거리만 봅니다. 검색 품질이 나쁘면 대개 임베딩 모델 문제이지 Vector DB 문제가 아닙니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;벡터 검색이 항상 키워드 검색보다 낫지는 않습니다.&lt;/strong&gt; 고유명사, 제품번호, 코드 같은 토큰은 BM25 같은 lexical 검색이 더 정확합니다. 그래서 실제 RAG는 &lt;strong&gt;벡터 + 키워드 하이브리드&lt;/strong&gt;가 기본입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;전용 Vector DB가 늘 필요하지도 않습니다.&lt;/strong&gt; PostgreSQL &lt;code&gt;pgvector&lt;/code&gt;, Elasticsearch/OpenSearch의 dense vector 필드, Redis vector search 모듈로도 상당 규모까지 커버됩니다. 운영 부담을 줄이려면 기존 DB 확장이 합리적일 때가 많습니다. Pinecone, Weaviate, Qdrant, Milvus 같은 전용 솔루션은 규모/성능 요구가 분명해질 때 고려하면 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;한국어_처리에서_중요한_구성요소&quot;&gt;3. 한국어 처리에서 중요한 구성요소&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한국어 RAG는 영어보다 손이 많이 갑니다. 어디서 차이 나는지 짚어봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;임베딩_모델의_한국어_이해_능력&quot;&gt;3.1. 임베딩 모델의 한국어 이해 능력&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 결정적인 부품입니다. OpenAI &lt;code&gt;text-embedding-3&lt;/code&gt;, Cohere &lt;code&gt;embed-multilingual&lt;/code&gt;, Voyage, BGE-M3 같은 다국어 임베딩이 점점 좋아지고 있지만, 한국어 전용 튜닝 모델(&lt;code&gt;KURE&lt;/code&gt;, &lt;code&gt;bge-m3-korean&lt;/code&gt;, 네이버 HyperCLOVA 임베딩 등)이 도메인에 따라 더 잘 맞기도 합니다. &lt;strong&gt;MTEB 한국어 벤치마크&lt;/strong&gt;를 참고하되, 자기 도메인 문서로 직접 평가해보는 게 가장 정확합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;토크나이저와_형태소_분석&quot;&gt;3.2. 토크나이저와 형태소 분석&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;벡터 검색 쪽&lt;/strong&gt;: 임베딩 모델 내부 토크나이저를 따라가므로 사용자가 손댈 여지가 적습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;키워드 검색 쪽&lt;/strong&gt;: 한국어에서 가장 손이 많이 가는 지점입니다. 영어는 공백 분리만으로도 어느 정도 작동하지만, 한국어는 &lt;strong&gt;조사/어미가 붙은 채로 인덱싱되면 검색이 망가집니다&lt;/strong&gt;. &quot;은행에서&quot;, &quot;은행이&quot;, &quot;은행은&quot;이 다 다른 토큰이 되니까요.&lt;/p&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Elasticsearch/OpenSearch는 &lt;strong&gt;Nori&lt;/strong&gt; 분석기, 그 외 &lt;strong&gt;Mecab-ko / Khaiii / KIWI&lt;/strong&gt; 같은 형태소 분석기를 식별자 추출/색인에 씁니다. &lt;strong&gt;사용자 사전(custom dictionary)&lt;/strong&gt; 관리가 매우 중요합니다. 신조어, 사내 용어, 제품명을 분석기가 모르면 엉뚱한 토큰으로 쪼개집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;청킹_전략&quot;&gt;3.3. 청킹 전략&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한국어 문서는 한 문장이 길고 접속사 없이 늘어지는 경우가 많아 &lt;strong&gt;고정 길이 청킹&lt;/strong&gt;이 의미 단위를 자주 끊습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;문장 분리(sentence splitting)&lt;/strong&gt; 가 마침표 한 종류만으로는 안 됩니다. &quot;다.&quot;, &quot;요.&quot;, &quot;음.&quot;, &quot;임.&quot; 같은 어미와 줄바꿈, 표/리스트 마크업까지 함께 봐야 합니다. KSS(Korean Sentence Splitter) 같은 라이브러리가 도움이 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;의미 단위 청킹(semantic chunking)&lt;/strong&gt; — 임베딩 유사도가 급격히 변하는 지점에서 자르기 — 이 한국어에서 효과가 좋은 편입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;계층적 청킹&lt;/strong&gt;: 큰 청크로 검색하고 매칭된 부분의 작은 청크를 LLM에 넘기는 식. 문맥 의존성이 강한 한국어에 특히 유용합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;재순위rerank_모델&quot;&gt;3.4. 재순위(rerank) 모델&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cross-encoder 기반 reranker는 검색 결과를 다시 정렬해 정확도를 크게 끌어올립니다. &lt;strong&gt;Cohere Rerank 다국어 모델, BGE-reranker, Jina multilingual reranker&lt;/strong&gt; 등이 한국어를 다룹니다. 한국어 전용 reranker는 아직 선택지가 적어, 다국어 모델로 시작해 자체 평가셋으로 fine-tune을 검토하는 흐름이 흔합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;쿼리_처리&quot;&gt;3.5. 쿼리 처리&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한국어 질문은 구어체, 줄임말, 오타가 많습니다. &lt;strong&gt;쿼리 재작성(query rewriting)&lt;/strong&gt; 이나 &lt;strong&gt;HyDE(hypothetical document embedding)&lt;/strong&gt; 로 LLM을 한 번 거쳐 정제하면 검색 품질이 눈에 띄게 좋아집니다. 이때 쓰는 LLM은 작아도 충분합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;rag_안에서_쓰이는_llm들&quot;&gt;4. RAG 안에서 쓰이는 LLM들&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;RAG에 LLM 하나&quot;라고 생각하기 쉽지만, 실제 시스템에는 &lt;strong&gt;서로 다른 역할의 모델 여러 개&lt;/strong&gt;가 들어가고, 각각 요구되는 특성도 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;위치&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;역할&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;필요한 특성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;임베딩 모델&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;텍스트를 벡터로 변환&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;다국어/한국어 품질, 적절한 차원 수, 빠른 추론 속도, 긴 입력 지원&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;쿼리 재작성 / 분기&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용자 질문을 정제하거나 어떤 도구/인덱스를 쓸지 결정&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;작고 빠른 instruction-following 모델로 충분. 지연 시간이 중요&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;재순위 모델&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;검색된 후보 K개를 정밀 점수화&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cross-encoder. 일반 LLM이 아니라 전용으로 학습된 reranker가 효율적&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;응답 생성 LLM&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;검색된 문맥을 바탕으로 최종 답변 작성&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;긴 컨텍스트, 인용 능력, 환각 억제, 한국어 자연스러움. 대형 모델이 유리&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;평가 / 가드레일&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;응답이 컨텍스트에 충실한지, 안전한지 검증&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;비교적 작은 모델로 LLM-as-a-judge. 일관성과 비용이 중요&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;흔한 구성은 &lt;strong&gt;&quot;쿼리 처리/판정은 작고 빠른 모델, 최종 생성은 큰 모델&quot;&lt;/strong&gt; 입니다. 전부 큰 모델로 돌리면 비용과 지연이 누적되고, 전부 작은 모델로 돌리면 답변 품질이 무너집니다. 단계별 적정 사이즈 선택이 RAG 설계의 중요한 부분입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또 한 가지, &lt;strong&gt;검색 결과를 잘 활용하는 능력은 모델마다 다릅니다&lt;/strong&gt;. 같은 컨텍스트를 줘도 어떤 모델은 인용에 충실하고, 어떤 모델은 사전 지식으로 답을 만들어버립니다. RAG 평가 때는 모델의 &lt;strong&gt;groundedness&lt;/strong&gt;(컨텍스트 충실도)를 따로 측정하는 게 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;성능이_안_나올_때_어디를_손볼_것인가&quot;&gt;5. 성능이 안 나올 때 어디를 손볼 것인가&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;RAG 결과가 만족스럽지 않을 때, 무작정 더 큰 LLM으로 갈아끼우기 전에 &lt;strong&gt;파이프라인의 어느 단계가 망가졌는지&lt;/strong&gt; 진단하는 게 우선입니다. 흔한 원인을 단계별로 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1단계_검색이_잘못_가져온다_retrieval_failure&quot;&gt;5.1. 1단계 — 검색이 잘못 가져온다 (retrieval failure)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 흔한 원인입니다. &lt;strong&gt;답이 든 문서를 애초에 못 찾아왔다면, 뒤에 어떤 LLM을 써도 좋아질 수 없습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;진단: 정답이 든 청크가 top-K 안에 들어오는지 확인. 안 들어오면 검색 단계 문제입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;손볼 곳:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;임베딩 모델 교체&lt;/strong&gt;: 한국어 품질이 더 좋은 모델로. 도메인 특화 모델 검토.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;하이브리드 검색 도입&lt;/strong&gt;: 벡터 + BM25 결합. 고유명사/숫자 매칭이 안 되던 케이스가 풀립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;청킹 재설계&lt;/strong&gt;: 너무 크면 노이즈, 너무 작으면 문맥이 끊깁니다. 한국어는 보통 300~800 토큰에서 시작해 조정.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;메타데이터 필터&lt;/strong&gt;: 검색 공간을 좁히는 가장 효과적인 수단. 날짜, 출처, 권한 등을 적극 활용.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;사용자 사전&lt;/strong&gt;: 키워드 검색 쪽 도메인 용어가 잘못 토큰화되는지 점검.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2단계_가져오긴_했는데_순서가_나쁘다_ranking_failure&quot;&gt;5.2. 2단계 — 가져오긴 했는데 순서가 나쁘다 (ranking failure)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;top-20에는 정답이 있는데 top-3에는 없는 경우. LLM에 넘기는 컨텍스트가 한정돼 있어 순서가 결과를 좌우합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;손볼 곳:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reranker 추가&lt;/strong&gt;: cross-encoder reranker가 가장 큰 효과를 내는 지점입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;쿼리 재작성&lt;/strong&gt;: 모호하거나 구어체인 질문을 검색에 유리한 형태로 변환.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;다중 쿼리 (multi-query)&lt;/strong&gt;: 한 질문을 여러 변형으로 던져 결과를 합칩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;3단계_컨텍스트는_좋은데_생성이_망가진다_generation_failure&quot;&gt;5.3. 3단계 — 컨텍스트는 좋은데 생성이 망가진다 (generation failure)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;검색은 잘 됐고 정답 청크가 top에 있는데 최종 응답이 틀리거나 환각이 섞이는 경우.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;손볼 곳:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 구조&lt;/strong&gt;: &quot;주어진 문맥에만 근거해 답하라&quot;, &quot;문맥에 없으면 모른다고 답하라&quot; 같은 지시. 인용 형식 강제.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;컨텍스트 배치&lt;/strong&gt;: 중요 문서를 맨 앞이나 맨 뒤에. &quot;lost in the middle&quot; 회피.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;생성 모델 업그레이드&lt;/strong&gt;: 여기서는 큰 모델이 효과적. 단, 1~2단계가 망가져 있으면 모델만 키워도 효과가 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;citation / grounded generation&lt;/strong&gt;: 답변마다 출처 청크를 명시하게 하면 환각이 줄어듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;4단계_평가가_없어서_어디가_망가졌는지_모른다_evaluation_gap&quot;&gt;5.4. 4단계 — 평가가 없어서 어디가 망가졌는지 모른다 (evaluation gap)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실은 가장 근본적인 문제입니다. &lt;strong&gt;평가셋이 없으면 변경이 개선인지 알 수 없습니다.&lt;/strong&gt; &quot;감으로&quot; 튜닝하다 보면 한쪽을 고치며 다른 쪽이 망가집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;손볼 곳:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;질문-정답-출처 셋으로 된 평가 데이터셋&lt;/strong&gt; 구축. 50~100개로도 시작 가능.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;단계별 지표&lt;/strong&gt;: 검색은 recall@K, MRR. 생성은 faithfulness, answer relevance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ragas, TruLens&lt;/strong&gt; 같은 평가 프레임워크 활용.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LLM-as-a-judge&lt;/strong&gt;로 자동화하되 사람이 주기적으로 보정.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;어디부터_손댈지&quot;&gt;6. 어디부터 손댈지&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;새 RAG 시스템을 만들거나 기존 것을 개선할 때 제가 보는 우선순위는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;평가셋부터 만든다.&lt;/strong&gt; 없으면 개선이 개선인지 모릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;하이브리드 검색을 기본으로.&lt;/strong&gt; 벡터만 쓰지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;한국어라면 형태소 분석기와 사용자 사전 정비.&lt;/strong&gt; 키워드 검색의 비중이 의외로 큽니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;reranker 도입.&lt;/strong&gt; 비용 대비 효과가 큰 편입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;생성 모델 업그레이드는 마지막에.&lt;/strong&gt; 가장 비싸고, 1~3이 부실하면 효과가 작습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;RAG의 성능 개선은 &lt;strong&gt;&quot;더 큰 모델&quot;이 아니라 &quot;더 정확한 검색&quot;&lt;/strong&gt; 에서 오는 경우가 대부분입니다. 앞 글에서 RAG를 &quot;프롬프트 레이어로 데이터를 흘려보내는 파이프라인&quot;이라 부른 이유도 여기 있습니다. 모델은 토큰으로 들어온 것에 답할 뿐, 들어오지 않은 것에는 답할 수 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude(claude.ai)와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>LLM에 정보를 전달하는 계층 분류와 회색지대</title>
      <link>https://blog.benelog.net//llm-information-layers.html</link>
      <pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">llm-information-layers.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#모델_레이어&quot;&gt;1. 모델 레이어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#프롬프트_레이어&quot;&gt;2. 프롬프트 레이어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#회색지대&quot;&gt;3. 회색지대&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#두_레이어_외에_추가로_생각해볼_만한_축&quot;&gt;4. 두 레이어 외에 추가로 생각해볼 만한 축&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rag는_어느_레이어인가&quot;&gt;5. RAG는 어느 레이어인가&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;LLM(Large Language Model)에 정보를 전달하는 방법을 정리할 때 저는 보통 &lt;strong&gt;모델 가중치 vs. 프롬프트&lt;/strong&gt;의 2계층으로 나눠 생각해 왔습니다.
최근 몇 년 사이에 그 중간에 위치하는 기법들도 늘었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock text-center&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/llm-information-layers/mindmap.drawio.png&quot; alt=&quot;LLM에 정보를 전달하는 계층 마인드맵&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. LLM에 정보를 전달하는 계층 분류&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;모델_레이어&quot;&gt;1. 모델 레이어&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서 말하는 &lt;strong&gt;모델 파라미터(model parameter)&lt;/strong&gt; 는 함수 인자나 설정값이 아니라, 신경망 내부에 학습으로 결정된 숫자값입니다. 입력 토큰을 다음 토큰의 확률로 변환하는 모든 계산이 이 숫자들로 이뤄집니다. 대부분은 레이어 간 연결 강도인 &lt;strong&gt;가중치(weight)&lt;/strong&gt; 이고, 편향(bias) 등이 더해집니다. 물리적으로는 낱개가 아니라 &lt;strong&gt;벡터/행렬(텐서) 형태&lt;/strong&gt;로 묶여 GPU에서 행렬 곱으로 한꺼번에 계산됩니다. 요즘 LLM은 이 숫자가 수십억~수조 개 수준이며, 학습은 결국 이 숫자를 조금씩 바꾸는 과정입니다. 모델이 &quot;아는 것&quot;은 이 파라미터 안에 압축돼 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 레이어의 방법들은 가중치를 직접 바꾸거나, 가중치에 준하는 영구적 상태를 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pre-training&lt;/strong&gt;: 가장 근본적인 정보 주입. 코퍼스의 분포가 곧 모델의 세계관이 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Continued pre-training (도메인 적응)&lt;/strong&gt;: 의료/법률/코드 등 특정 도메인 코퍼스로 추가 학습. 파인튜닝보다 광범위한 분포 이동을 노립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Supervised fine-tuning (SFT)&lt;/strong&gt;: 입출력 페어로 행동을 맞춥니다. 지식 주입보다 형식/스타일/태스크 수행에 효과적입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;선호 학습 (preference learning)&lt;/strong&gt;: &quot;어떤 응답이 더 나은가&quot;를 가중치에 새깁니다. 지식이 아니라 &lt;strong&gt;가치/취향&lt;/strong&gt; 주입이라는 점에서 결이 다릅니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RLHF(Reinforcement Learning from Human Feedback): 사람이 매긴 선호로 보상 모델을 학습한 뒤, 그 보상을 최대화하도록 PPO 등 강화학습으로 정책을 업데이트하는 고전적 방식&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DPO(Direct Preference Optimization): 별도의 보상 모델과 강화학습 루프 없이, 선호 쌍(채택/기각)으로부터 정책을 직접 최적화해 RLHF를 단순화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;KTO(Kahneman-Tversky Optimization): 쌍 비교 대신 응답 하나가 &quot;좋다/나쁘다&quot;는 단일 라벨만으로 학습. 전망 이론(prospect theory)의 효용 함수를 차용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IPO(Identity Preference Optimization): DPO가 선호 데이터에 과적합되는 경향을 막도록 목적 함수에 정규화 항을 더한 변형&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RLVR(Reinforcement Learning with Verifiable Rewards)&lt;/strong&gt;: 수학 정답, 코드 실행 결과처럼 자동 채점 가능한 신호로 강화학습. DeepSeek-R1 계열은 이 방식이고, o1도 강화학습 기반 추론 모델로 알려져 있지만 보상 설계의 세부는 공개되지 않았습니다. 사람의 선호가 아니라 &lt;strong&gt;풀이 능력 자체&lt;/strong&gt;를 새긴다는 점에서 RLHF와 구분됩니다. Cursor의 Composer 2.5는 이를 에이전트 환경으로 확장한 예로, 도구(편집·검색·테스트 실행)를 호출하는 멀티스텝 롤아웃에서 &lt;strong&gt;테스트 통과 여부&lt;/strong&gt;를 검증 가능한 보상으로 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Model editing&lt;/strong&gt;: 특정 사실 하나를 가중치 차원에서 직접 수정. &quot;에펠탑은 로마에 있다&quot;로 바꾸는 수술 같은 기법입니다. 본격적 학습이 아니어서 회색지대로 보기도 합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ROME(Rank-One Model Editing): 해당 사실을 저장한 MLP 가중치에 rank-1 업데이트를 가해 사실 하나를 갈아끼움&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MEMIT(Mass-Editing Memory In a Transformer): ROME을 다중 레이어로 확장해 수천 개의 사실을 동시에 편집&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MEND(Model Editor Networks with Gradient Decomposition): 메타 학습된 보조 네트워크가 편집용 그라디언트로 변환해 빠르게 적용&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Knowledge distillation&lt;/strong&gt;: 큰 모델의 지식을 작은 모델의 가중치로 압축.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Merging&lt;/strong&gt;: 여러 파인튜닝 결과의 가중치를 평균/합성. 학습 없이 &quot;정보를 섞는다&quot;는 점에서 새로운 결입니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;TIES(TrIm, Elect Sign &amp;amp; Merge): 변화가 작은 파라미터는 잘라내고, 부호 충돌을 다수결로 정한 뒤 평균&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DARE(Drop And REscale): 파인튜닝 델타의 대부분을 무작위로 버리고 남은 값을 재스케일링해 머지&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SLERP(Spherical Linear intERPolation): 두 모델 가중치를 단위 구면상 호로 보간&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;프롬프트_레이어&quot;&gt;2. 프롬프트 레이어&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;해당 레이어는 추론 시점에 컨텍스트 윈도우로 들어가는 모든 것입니다.가중치는 건드리지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;시스템 프롬프트 / 페르소나 지시&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Few-shot 예시 (in-context learning)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Chain-of-Thought, ReAct(Reasoning + Acting), self-consistency 같은 reasoning 패턴 유도&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tool use / function calling의 결과를 컨텍스트로 되먹임&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RAG(Retrieval-Augmented Generation)로 검색해 온 문서 청크&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;외부 메모리 시스템에서 불러온 과거 대화 (Claude의 memory 같은 것)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;구조화된 출력 지시 (XML 태그, JSON 형식 지시)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MCP(Model Context Protocol)/connector를 통해 들어오는 외부 데이터&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;공통점은 &lt;strong&gt;inference time에 토큰으로 입력된다&lt;/strong&gt;는 것입니다. 모델은 최종적으로 토큰과 role/구조 신호를 보고 처리하며, 외부 데이터의 원천 신뢰도를 직접 검증하지는 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;회색지대&quot;&gt;3. 회색지대&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모델 레이어와 프롬프트 레이어 어디에도 깔끔하게 들어가지 않는 기법들입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PEFT(Parameter-Efficient Fine-Tuning) 계열&lt;/strong&gt;: 가중치를 학습시키니 형식상 모델 레이어지만, 베이스 모델은 두고 작은 어댑터만 붙였다 뗐다 합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;LoRA(Low-Rank Adaptation) / QLoRA(Quantized LoRA): 파인튜닝으로 생기는 가중치 변화(ΔW)를 두 작은 행렬의 곱(B·A)인 저랭크(low-rank) 행렬로 근사해, 원래 가중치는 동결하고 이 작은 행렬만 학습. 가중치 변화가 실은 소수의 지배적인 방향만으로 충분히 표현된다(intrinsic rank가 낮다)는 가정에 기댑니다. 고차원 데이터를 분산이 큰 몇 개의 주성분으로 압축하는 &lt;strong&gt;PCA/SVD의 저랭크 근사와 같은 발상&lt;/strong&gt;으로, 다만 ΔW를 SVD로 직접 분해하는 대신 두 행렬을 경사하강법으로 학습한다는 점이 다릅니다. QLoRA는 여기에 베이스 모델 양자화(정밀도 축소)를 더해, &lt;strong&gt;랭크 축소(LoRA)와 정밀도 축소(양자화)라는 직교하는 두 압축 축을 함께 쓰는 셈&lt;/strong&gt;입니다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;adapters: 트랜스포머 레이어 사이에 작은 학습 모듈을 끼워 넣고 그 부분만 학습&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;prefix tuning / prompt tuning / P-tuning: 학습 가능한 &quot;가상 토큰&quot;을 컨텍스트 앞에 붙이는 방식이라 &lt;strong&gt;소프트 프롬프트(soft prompt)&lt;/strong&gt; 라 불릴 만큼 프롬프트 레이어의 사촌&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;KV(Key-Value) cache 주입 / cache steering&lt;/strong&gt;: 특정 컨텍스트로 만든 KV cache를 저장해 두고 추론 시 재사용. 매번 프롬프트로 넣지는 않지만, 이미 계산된 컨텍스트를 다시 붙이는 효과가 있습니다. 일반적으로는 비용 최적화용 런타임 상태이며, 모델 가중치나 장기 메모리와는 구분됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Activation steering / representation engineering&lt;/strong&gt;: 추론 중 특정 레이어의 활성값에 벡터를 더해 행동을 바꿉니다. 가중치도 프롬프트도 건드리지 않는 완전히 새로운 결입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Decoding 제어&lt;/strong&gt;: 출력 확률 분포를 직접 조작. &quot;주입&quot;보다 &lt;strong&gt;출력 제한&lt;/strong&gt;에 가깝지만, 도메인 지식을 grammar로 강제하면 사실상 정보 주입입니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;constrained decoding / guided generation: 정해진 문법(grammar)·스키마를 따르도록 토큰 선택을 제한해 출력 형식을 강제&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;structured outputs: JSON Schema 같은 스키마를 입력으로 주되, strict 모드에서는 서버/런타임의 constrained decoding과 결합되어 프롬프트 레이어와 디코딩 레이어에 걸침&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;logit bias: 특정 토큰의 로짓에 값을 가감해 등장 확률을 직접 조절&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;speculative decoding: 작은 드래프트 모델로 여러 토큰을 미리 뽑고 본 모델로 한 번에 검증. 정보 주입이 아니라 속도 최적화에 가까움&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tool use의 양면성&lt;/strong&gt;: 도구 호출 자체는 프롬프트 레이어지만, 도구가 외부 세계의 상태를 바꾼다면 &quot;LLM에 정보를 주는 것&quot;이 아니라 &quot;LLM이 정보를 만들어내는 것&quot;입니다. 경계가 모호합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;두_레이어_외에_추가로_생각해볼_만한_축&quot;&gt;4. 두 레이어 외에 추가로 생각해볼 만한 축&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 2계층 분류는 &lt;strong&gt;정보가 어디에 저장되는가&lt;/strong&gt; 기준입니다. 다른 축도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;아키텍처 레이어&lt;/strong&gt;:
모델 구조 자체. MoE(Mixture of Experts)로 전문가를 분리하거나, 검색 모듈을 구조에 통합하거나, 메모리 토큰 슬롯을 두는 식.
가중치 학습 이전에 &quot;&lt;strong&gt;어떤 형태로 정보를 담을 것인가&lt;/strong&gt;&quot;를 정합니다. retrieval-augmented architecture/pretraining의 예:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;REALM(Retrieval-Augmented Language Model pre-training): 사전학습 단계부터 retriever를 함께 학습해 외부 코퍼스 검색을 모델에 통합&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RETRO(Retrieval-Enhanced TRansfOrmer): 트랜스포머 블록에 검색된 청크의 chunked cross-attention을 끼워 넣어 작은 모델로 큰 효과&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;표현 / 활성값 레이어&lt;/strong&gt;:
앞서 언급한 activation steering, probing, representation engineering. 가중치도 컨텍스트도 아닌 &lt;strong&gt;forward pass 중간 상태&lt;/strong&gt;에 개입합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;디코딩 / 샘플링 레이어&lt;/strong&gt;:
temperature, top-p, constrained decoding, beam search. 정보 주입이 아니라 추출 방식 제어지만, 모델이 &quot;무엇을 말할 수 있는가&quot;를 좌우합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;오케스트레이션 / 에이전트 레이어&lt;/strong&gt;:
여러 LLM 호출을 엮는 외부 통제 흐름. 단일 추론에서는 안 보이지만 시스템 전체 동작을 결정합니다. 멀티 에이전트, 워크플로우, planner-executor 패턴 등.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;rag는_어느_레이어인가&quot;&gt;5. RAG는 어느 레이어인가&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;RAG는 결국 프롬프트 레이어에 데이터를 공급하는 장치입니다. 검색해 온 청크는 컨텍스트에 토큰으로 들어갑니다.
이 관점이 실용적인 이유는 RAG의 한계가 곧 &lt;strong&gt;프롬프트 레이어의 한계&lt;/strong&gt;(컨텍스트 윈도우, 어텐션의 long-range 약점, 토큰 비용, &quot;lost in the middle&quot;)로 환원되기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 RAG를 &lt;strong&gt;별개의 레이어로 보는 시각&lt;/strong&gt;도 일리가 있습니다. 두 가지 이유입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;시스템 구성요소가 훨씬 많음&lt;/strong&gt;.
임베딩 모델, 벡터 DB, 청킹 전략, 재순위 모델, 쿼리 재작성 등 프롬프트 엔지니어링과 결이 다른 &lt;strong&gt;검색 시스템 설계&lt;/strong&gt; 영역입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;모델과 무관하게 진화함&lt;/strong&gt;.
베이스 LLM이 그대로여도 RAG 파이프라인 개선만으로 성능이 오릅니다.
독립적 최적화 표면이 있다는 뜻이고, 별도 레이어로 다룰 실용적 이유가 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉 RAG는 개념적으로는 프롬프트 레이어의 하위 범주, 실무적으로는 독립된 서브시스템입니다.
&quot;LLM에 정보가 들어가는 경로&quot;를 논할 때는 프롬프트 레이어로 묶고, &quot;이 RAG 파이프라인을 어떻게 개선할까&quot;를 논할 때는 별도 레이어로 다루는 식입니다.
&lt;strong&gt;세상에 있는 데이터 → 검색/필터링 → 프롬프트 윈도우&lt;/strong&gt; 흐름에서 앞단(검색/필터링)이 충분히 복잡해 별도 시스템이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 논리로 &lt;strong&gt;memory 시스템, MCP, tool use 결과 통합&lt;/strong&gt;도 &quot;프롬프트 레이어로 흘러드는 데이터 파이프라인&quot;이라는 같은 범주입니다.
이들을 묶어 &lt;strong&gt;컨텍스트 공급 레이어(context provisioning layer)&lt;/strong&gt; 라 부르면, 처음의 2계층이 자연스럽게 3계층으로 확장됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;모델 레이어 (파라미터)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;컨텍스트 공급 레이어 (RAG, 메모리, 도구, 커넥터)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프롬프트 레이어 (실제 컨텍스트에 들어간 토큰의 구성과 배치)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기에 회색지대(PEFT, activation steering)와 아키텍처/디코딩 축까지 보태면, 단순했던 2계층 지도가 더 풍부하게 세분화됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude(claude.ai)와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Hermes와 Modal로 AI agent 챗봇을 0원에 운영하기</title>
      <link>https://blog.benelog.net//hermes-modal-telegram-bot.html</link>
      <pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">hermes-modal-telegram-bot.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#챗봇의_기능&quot;&gt;1. 챗봇의 기능&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#구성_요소&quot;&gt;2. 구성 요소&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#llm_엔진&quot;&gt;2.1. LLM 엔진&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#modal의_운영_모델&quot;&gt;2.2. Modal의 운영 모델&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#챗봇_앱의_핵심_코드&quot;&gt;3. 챗봇 앱의 핵심 코드&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#최소_재현_절차&quot;&gt;3.1. 최소 재현 절차&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#메시지가_올_때만_프로세스가_깨어나게&quot;&gt;3.2. 메시지가 올 때만 프로세스가 깨어나게&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#갱신_주기에_따른_volume_분리&quot;&gt;3.3. 갱신 주기에 따른 Volume 분리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#지식_db_소스&quot;&gt;3.4. 지식 DB 소스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#연동_시스템을_위한_secret_관리&quot;&gt;3.5. 연동 시스템을 위한 Secret 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#idempotent한_부팅_스크립트&quot;&gt;3.6. idempotent한 부팅 스크립트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#런타임에_추가되는_skill을_git으로_관리하기&quot;&gt;3.7. 런타임에 추가되는 skill을 git으로 관리하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#배포_운영&quot;&gt;4. 배포, 운영&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#webhook_url을_위한_두_단계_배포&quot;&gt;4.1. webhook URL을 위한 두 단계 배포&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#chatgptcodex_oauth_refresh_token_충돌&quot;&gt;4.2. ChatGPT(Codex) OAuth refresh token 충돌&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#핵심_팁_정리&quot;&gt;5. 핵심 팁 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;6. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 GitHub에 정리한 개인 기록을 근거로 답하는 텔레그램 챗봇을 &lt;strong&gt;고정 비용 0원&lt;/strong&gt;으로 운영하고 싶었습니다.
항상 켜 둘만한 개인 PC가 있지도 않았습니다.
그래서 LLM 에이전트인 Hermes와 서버리스 플랫폼인 Modal, Telegram 웹훅 등의 여러 기술을 조합하여 챗봇을 띄웠습니다.
그 선택 과정과 핵심 코드를 이 글에서 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 구성은 &lt;strong&gt;트래픽이 많지 않고&lt;/strong&gt;, Modal의 월별 무료 크레딧 안에서 호출과 저장소 사용량이 감당되며, K-QMD 임베딩 갱신을 아주 자주 돌리지 않는다는 전제에서 0원에 가깝게 운영할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;챗봇의_기능&quot;&gt;1. 챗봇의 기능&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 그동안 개인적으로 기록해둔 블로그, 독서 감상문, 일기, 개발 관련 링크 모음은 Markdown이나 Asciidoc 형태로 GitHub 저장소에 쌓여 있습니다. 일종의 개인 지식 DB입니다.
해당 지식 저장소들을 한 번에 검색해서 대답을 하는 Telegram 챗봇을 만들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같은 질문들에 재미있는 답을 해주었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;내가 가장 재미있게 읽은 만화책은?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;내가 가장 감명 깊게 보낸 한 해는?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;나의 주 관심사와 어울리는 책을 추천해줘.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/chat-bot/telegram-chat-bot.png&quot; alt=&quot;Telegram 챗봇 응답 예시&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;구성_요소&quot;&gt;2. 구성 요소&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;챗봇은 다음과 같은 구성요소로 설계했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;컴포넌트&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;역할&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Hermes&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;LLM 에이전트&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Telegram webhook gateway, MCP 클라이언트, 에이전트 루프를 모두 제공&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;K-QMD&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;검색 엔진&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Markdown, Asciidoc 을 임베딩으로 인덱싱하고 의미 검색을 MCP로 제공. QMD에 Kiwi 형태소 분석을 결합한 fork&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Modal&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실행 환경&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;컨테이너 실행, 데이터 저장 공간&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OpenClaw와 Hermes를 비교하는 글에서 Hermes가 클라우드에 올리기 편하다는 설명을 보고 선택했습니다.
검색 엔진은 처음엔 QMD를 썼는데, 한글 문서에서 검색 recall이 낮아서 한글 형태소 분석기를 결합한 &lt;a href=&quot;https://github.com/jylkim/kqmd&quot;&gt;K-QMD&lt;/a&gt; fork로 옮겼습니다. K-QMD는 binary 이름과 CLI를 그대로 유지하는 drop-in replacement라 교체 비용이 거의 없었습니다.
Modal은 Gemini의 추천을 받아서 선택했습니다.
Hermes의 소개글에서 클라우드 실행 환경으로는 Daytona와 Modal을 사용할 수 있다는 설명을 먼저 보고, Gemini에게 무료로 챗봇 운영을 하기에 유리한 플랫폼을 추천해달라고 요청한 결과입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;LLM 에이전트는 처음엔 OpenClaw를 썼는데, 위 구성으로 가려고 Hermes로 옮겼습니다. Hermes가 OpenClaw 데이터 마이그레이션을 지원해서 큰 어려움은 없었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;llm_엔진&quot;&gt;2.1. LLM 엔진&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;LLM 엔진은 GPT 5.5로 연동했습니다.
평소 주로 쓰는 모델은 Claude 계열이지만, 해당 시점에서는 개인 구독하는 Claude 상품이 Hermes와 연동이 되지 않아서 ChatGPT 구독을 통한 GPT 5.5를 선택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;modal의_운영_모델&quot;&gt;2.2. Modal의 운영 모델&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Modal은 실제 사용자 호출을 처리하는 동안에만 컨테이너가 깨어나는 서버리스 플랫폼입니다.
대기 시간에는 비용이 발생하지 않고, 현재 Starter 플랜에서는 월 $30의 무료 크레딧이 제공되므로 트래픽이 띄엄띄엄한 Telegram 챗봇은 그 범위 안에서 0원으로 운영할 수 있을 것으로 예상합니다.
대신 서버가 깨어나는 첫 호출에는 cold start 지연이라는 트레이드오프가 있습니다. 첫 메시지는 몇 초~십수 초 늦게 답이 옵니다. 챗봇이 지연에 민감하지 않다면 받아들일 만한 단점입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OpenClaw에서는 기본적으로는 long polling으로 Telegram과 연동을 합니다.
long polling을 하기 위해서는 서버가 계속 켜져 있어야 하기에 Modal을 통해 추구하는 &quot;필요할 때만 깨어나는&quot; 운영 모델과는 맞지 않았습니다. 그래서 Hermes로 전환하면서 Telegram의 webhook을 통해 연동하는 구조로 바꿨습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;챗봇_앱의_핵심_코드&quot;&gt;3. 챗봇 앱의 핵심 코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전체 소스는 &lt;a href=&quot;https://github.com/benelog/hermes-modal&quot;&gt;&lt;code&gt;benelog/hermes-modal&lt;/code&gt;&lt;/a&gt; 저장소에 있습니다. 이 글에서는 그중 설계 의도를 보여주는 코드 조각만 발췌해 설명합니다. Modal로 실행하는 파이썬 앱의 설계와 구현은 Claude Code의 도움을 받았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;최소_재현_절차&quot;&gt;3.1. 최소 재현 절차&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음부터 같은 구성을 따라 하려면 아래 순서로 접근하는 편이 시행 착오가 적습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;Telegram bot token, LLM provider 인증 정보, GitHub token을 준비합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Modal의 Secret과 Volume을 생성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;TELEGRAM_WEBHOOK_URL&lt;/code&gt; 없이 먼저 &lt;code&gt;modal deploy&lt;/code&gt; 해서 외부 URL을 발급받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;발급받은 URL을 Telegram webhook과 Modal secret에 반영한 뒤 다시 &lt;code&gt;modal deploy&lt;/code&gt; 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`modal run modal_app.py::sync_qmd --embed`를 실행해서 지식 저장소를 가져오고 인덱스를 만듭니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이후에는 Telegram에서 메시지를 보내 챗봇을 호출하고, 문서가 바뀌면 `sync_qmd`만 다시 돌리면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;메시지가_올_때만_프로세스가_깨어나게&quot;&gt;3.2. 메시지가 올 때만 프로세스가 깨어나게&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/modal_app.py&quot;&gt;modal_app.py&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;@app.function(
    image=image,
    secrets=[secret, github_secret],
    volumes=volume_mounts,
    timeout=60 * 60,
    max_containers=1,
    scaledown_window=60 * 10,
    env=common_env,
)
@modal.concurrent(max_inputs=50)
@modal.web_server(WEBHOOK_PORT, startup_timeout=120)
def gateway():
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;`min_containers`를 &lt;strong&gt;지정하지 않았습니다.&lt;/strong&gt; 24/7 고정 컨테이너가 없어 대기 시의 비용이 0이 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`scaledown_window=600`으로 10분 동안 요청이 없으면 자동 종료합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`max_containers=1`로 두 개 이상 컨테이너가 동시에 webhook을 받지 않게 막았습니다. Telegram bot token은 webhook을 받는 주체가 둘 이상이면 동작이 불안정합니다. 한 컨테이너 안에서의 동시 처리는 `@modal.concurrent(max_inputs=50)`로 받습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;갱신_주기에_따른_volume_분리&quot;&gt;3.3. 갱신 주기에 따른 Volume 분리&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;영속적으로 저장되어야할 파일은 Modal의 Volume에 저장했습니다. 이를 이용해서 Hermes 설정 파일, K-QMD 인덱스, 지식 DB 역할의 Git 저장소가 컨테이너 재시작과 무관하게 유지됩니다.
Volume은 아래 4개로 나눴습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/modal_app.py&quot;&gt;modal_app.py&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;hermes_volume    = modal.Volume.from_name(&quot;hermes-home&quot;, ...)
workspace_volume = modal.Volume.from_name(&quot;hermes-workspace&quot;, ...)
qmd_cache_volume = modal.Volume.from_name(&quot;qmd-cache&quot;, ...)
qmd_config_volume = modal.Volume.from_name(&quot;qmd-config&quot;, ...)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 Volume의 수명과 갱신 주기가 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hermes-home&lt;/code&gt;: Hermes config, runtime skill, OAuth &lt;code&gt;auth.json&lt;/code&gt;. 크기는 작고, 컨테이너 재시작마다 일부가 갱신됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;hermes-workspace&lt;/code&gt;: K-QMD가 인덱싱하는 git 저장소들. 크기가 가장 크고, &lt;code&gt;sync_qmd&lt;/code&gt; 호출 때만 갱신됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;qmd-cache&lt;/code&gt;: 임베딩/리랭크 GGUF 모델과 캐시. 거의 안 바뀌지만 용량이 큽니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;qmd-config&lt;/code&gt;: K-QMD index 설정. 작고 자주 바뀌지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한 Volume에 다 몰아넣으면 데이터 저장, 갱신을 할 때 상관없는 큰 데이터까지 함께 끌려옵니다.
갱신 주기가 비슷한 것끼리 묶는 편이 안전하고 효율적입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;지식_db_소스&quot;&gt;3.4. 지식 DB 소스&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;봇이 답할 때 참고하는 문서는 &lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/scripts/qmd_repos.yml&quot;&gt;&lt;code&gt;scripts/qmd_repos.yml&lt;/code&gt;&lt;/a&gt;에 정의했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/scripts/qmd_repos.yml&quot;&gt;qmd_repos.yml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;repos:
  - name: blog
    repo_url: git@github.com:benelog/blog.git
    branch: master
    collection_path: src/content
    pattern: &quot;**/*.{md,adoc}&quot;
  - name: wiki
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sync_qmd&lt;/code&gt; 함수는 이 manifest를 기준으로 repo를 clone/pull하고, repo 내부의 `collection_path`만 K-QMD config에 연결합니다. 내가 평소 쓰는 노트 저장소(블로그, 위키, devnote, 책장, 일기 등)가 그대로 챗봇의 지식 베이스가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문서를 갱신하고 싶을 때는 텔레그램 채팅창에서 봇에게 &quot;qmd 갱신해줘&quot;, &quot;최신 내용 가져와&quot; 같이 요청합니다. &lt;code&gt;sync-qmd&lt;/code&gt; 스킬이 git pull과 K-QMD 인덱싱을 실행해줍니다. 동일한 동작은 로컬에서 다음 명령으로도 트리거할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;modal run modal_app.py::sync_qmd          # repo만 갱신
modal run modal_app.py::sync_qmd --embed  # 임베딩까지&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이걸 cron이나 GitHub Actions로 주기 호출하면 챗봇 지식이 자동 최신화됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;연동_시스템을_위한_secret_관리&quot;&gt;3.5. 연동 시스템을 위한 Secret 관리&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;GitHub, ChatGPT, Telegram 등 다른 시스템과 연동을 하기 위한 Secret도 2개로 나눴습니다.
secret의 원본 저장소와 갱신하는 프로세스를 분리하기 위해서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ChatGPT, Telegram 인증을 위한 secret: `scripts/create_modal_secret.py`는 로컬 `~/.hermes/.env`와 `auth.json`에서 Modal secret을 만듭니다. 내부적으로 `modal secret create --force`를 써서 secret 전체를 통째로 교체합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GitHub Access token : Modal의 UI에서 직접 입력하도록 의도했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`modal_app.py`는 두 그룹의 secret을 함께 주입합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/modal_app.py&quot;&gt;modal_app.py&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;secret = modal.Secret.from_name(&quot;hermes-modal-secrets&quot;)
github_secret = modal.Secret.from_name(&quot;github-secret&quot;)

@app.function(secrets=[secret, github_secret], ...)
def gateway(): ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;idempotent한_부팅_스크립트&quot;&gt;3.6. idempotent한 부팅 스크립트&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gateway&lt;/code&gt;, &lt;code&gt;sync_qmd&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt; 함수 모두 시작할 때 `scripts/prepare_runtime.py`를 호출합니다. 이 스크립트가 하는 일은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;auth.json&lt;/code&gt; 작성 (base64로 secret에 들어 있는 OAuth 토큰)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hermes &lt;code&gt;config.yaml&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; 작성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;~/.hermes/SOUL.md&lt;/code&gt;, &lt;code&gt;~/.hermes/skills/&lt;/code&gt;, &lt;code&gt;~/.hermes/hooks/`를 image의 `scripts/&lt;/code&gt; 트리와 동기화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(옵션) 지식 repo clone/pull과 K-QMD 인덱싱&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;핵심은 &lt;strong&gt;컨테이너가 어떤 상태에서 시작하더라도 같은 결과&lt;/strong&gt;가 나오게 하는 것입니다. Modal Volume에 이전 실행의 부산물이 남아 있어도, image에서 새로 실어 보낸 skill이 있어도, 이 스크립트 한 번이면 정상 상태로 수렴합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;런타임에_추가되는_skill을_git으로_관리하기&quot;&gt;3.7. 런타임에 추가되는 skill을 git으로 관리하기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hermes는 실행 중에 새로운 skill이 생기거나 기존 skill이 업데이트될 수 있는 Agent입니다.
agent의 주인인 제가 그 변화를 추적하고 필요하면 직접 수정할 필요성도 있다고 판단했습니다.
그래서 Modal Volume에 저장되는 skill도 변화가 있을 때 git으로 자동으로 커밋되고 푸시되도록 구성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hermes가 실행 중 만든 skill을 다시 git repo로 push하는 흐름은 &lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/scripts/hooks/skills-autocommit/HOOK.yaml&quot;&gt;&lt;code&gt;scripts/hooks/skills-autocommit/HOOK.yaml&lt;/code&gt;&lt;/a&gt; 훅으로 걸어 두었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/scripts/hooks/skills-autocommit/HOOK.yaml&quot;&gt;HOOK.yaml&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;name: skills-autocommit
description: Push HERMES_HOME/skills changes back to the hermes-modal git repo after each agent turn.
events:
  - agent:end&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;agent:end&lt;/code&gt; 이벤트가 매 에이전트 종료마다 발생할 때 &lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/scripts/commit_skills.py&quot;&gt;&lt;code&gt;commit_skills.py&lt;/code&gt;&lt;/a&gt;가 호출됩니다. 이 스크립트는 Modal Volume의 &lt;code&gt;~/.hermes/skills/&lt;/code&gt; 변경분을 hermes-modal repo의 &lt;code&gt;scripts/skills/`로 복사하고 push합니다. 결과적으로 `git log`에 `Sync skills from Modal volume&lt;/code&gt; 커밋이 자동으로 쌓입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 구조에는 두 가지 의미가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;runtime에서 학습한 동작이 image로 흡수됩니다.&lt;/strong&gt; 다음 컨테이너 부팅 때 `prepare_runtime.py`가 image의 `scripts/skills/`를 그대로 Volume에 sync하므로, 결국 Volume은 image를 따라가게 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;에이전트가 스스로를 진화시키는 사이클&lt;/strong&gt;이 git에 그대로 남습니다. 어떤 skill이 언제 추가됐는지를 commit log로 확인하고, 사람이 review/revert 할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`commit_skills.py`에는 안전장치 두 개를 두었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/scripts/commit_skills.py&quot;&gt;commit_skills.py&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;LOCK_FILE = Path(&quot;/tmp/hermes-modal-commit-skills.lock&quot;)
FINGERPRINT_FILE = SKILLS_DIR / &quot;.last_committed_hash&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;flock&lt;/strong&gt;으로 동시에 여러 호출이 들어와도 직렬화합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SHA256 fingerprint&lt;/strong&gt;로 skill 트리가 안 바뀌었으면 git clone조차 하지 않고 빠르게 끝냅니다. `agent:end`는 매 에이전트 실행마다 발생하므로 변경이 없을 때의 비용을 0에 가깝게 두는 것이 중요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;디버깅 등 수동으로 트리거하고 싶을 때는 `modal run modal_app.py::commit_skills`를 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;배포_운영&quot;&gt;4. 배포, 운영&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;webhook_url을_위한_두_단계_배포&quot;&gt;4.1. webhook URL을 위한 두 단계 배포&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Modal에서 webhook 봇을 띄울 때 자주 부딪히는 닭-달걀 문제가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Telegram에 webhook URL을 등록하려면 Modal URL이 필요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그런데 Modal URL은 deploy 후에야 알 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 문제를 해결하기 위해 &lt;code&gt;gateway&lt;/code&gt; 함수가 `TELEGRAM_WEBHOOK_URL`이 비어 있으면 placeholder HTTP 서버만 띄우고 끝냅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal/blob/main/modal_app.py&quot;&gt;modal_app.py&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;if not os.environ.get(&quot;TELEGRAM_WEBHOOK_URL&quot;, &quot;&quot;).strip():
    print(&quot;TELEGRAM_WEBHOOK_URL is not set; starting placeholder server ...&quot;)
    subprocess.Popen([&quot;python&quot;, &quot;-m&quot;, &quot;http.server&quot;, str(WEBHOOK_PORT), ...])
    return

subprocess.Popen([&quot;bash&quot;, &quot;-lc&quot;,
    &quot;while true; do hermes gateway run --replace; ... done&quot;], env=os.environ.copy())&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;흐름은 이렇습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;webhook URL 없이 &lt;code&gt;modal deploy&lt;/code&gt; → Modal URL 발급&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그 URL을 secret에 넣고 다시 &lt;code&gt;modal deploy&lt;/code&gt; → Hermes가 정상 webhook 모드로 전환&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hermes를 &lt;code&gt;while true; do &amp;#8230;&amp;#8203; done&lt;/code&gt; 루프로 감싼 까닭은, 일시적인 네트워크 문제나 OAuth refresh 실패로 프로세스가 죽었을 때 곧바로 다시 띄우기 위해서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;chatgptcodex_oauth_refresh_token_충돌&quot;&gt;4.2. ChatGPT(Codex) OAuth refresh token 충돌&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;챗봇이 다음과 같이 에러 응답을 하는 상황이 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;text&quot;&gt;Provider authentication failed: Codex refresh token was already consumed by
another client (e.g. Codex CLI or VS Code extension). Run codex in your
terminal to generate fresh tokens, then run hermes auth to re-authenticate.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OAuth refresh token이 갱신되면서 Modal의 앱이 참조하는 토큰이 무효화된 것이 원인이었습니다.
당장의 복구는 ChatGPT 인증을 다시 하고 `~/.hermes/auth.json`을 새로 받아 Modal Secret을 다시 올리고 재배포하는 것입니다.
근본 해결은 provider를 OpenRouter/Anthropic/OpenAI API key 같은 단순 키 방식으로 바꾸는 것입니다.
토큰 회전이 없는 provider는 이 충돌이 아예 없습니다. 무료에 가깝게 쓰겠다는 처음 목표 때문에 OAuth provider를 일단 유지하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;핵심_팁_정리&quot;&gt;5. 핵심 팁 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 프로젝트를 통해 얻은 서버리스 플랫폼에서의 AI 챗봇 운영 팁은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;고정 비용을 피하려면 대기 비용을 최소화&lt;/strong&gt; : Modal에서는 `min_containers`를 두지 않고 `scaledown_window`만 짧게 잡는 것이 free tier 친화적입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;상태를 다시 만들 수 있게 부팅 과정을 idempotent하게&lt;/strong&gt; : Volume에 이전 실행 흔적이 남아도 &lt;code&gt;prepare_runtime.py&lt;/code&gt; 한 번으로 원하는 상태로 수렴하게 하는 것이 운영 부담을 줄입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;갱신 주기가 다른 데이터는 Volume도 분리&lt;/strong&gt; : 설정, 캐시, 지식 저장소를 따로 두면 큰 데이터를 괜히 함께 흔들지 않게 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;에이전트의 runtime 변경이 의미가 있다면 git으로 관리&lt;/strong&gt; : 서버 Volume에만 남으면 데이터를 잃기 쉽고, 사람이 추적, 수정 하기가 어렵습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Long polling 보다는 Webhook 방식으로&lt;/strong&gt; : 챗봇을 위한 프로세스가 항상 켜져 있을 필요가 없는 Webhook 방식이 서버리스 플랫폼과 더 잘 맞습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;초기 배포는 두 단계 : webhook URL이 deploy 이후에 생기는 구조라면 URL 확보를 위한 배포가 먼저 필요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;6. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/hermes-modal&quot;&gt;hermes-modal&lt;/a&gt; — 이 글에서 설명한 저장소&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://modal.com/docs&quot;&gt;Modal docs&lt;/a&gt; — Volume, Secret, web_server, concurrent 등의 공식 문서&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/NousResearch/hermes-agent&quot;&gt;Hermes Agent&lt;/a&gt; — Nous Research의 LLM 에이전트 프레임워크&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jylkim/kqmd&quot;&gt;K-QMD&lt;/a&gt; — QMD에 Kiwi 한글 형태소 분석을 결합한 fork (실제 사용 중)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tobilu/QMD&quot;&gt;QMD&lt;/a&gt; — K-QMD의 upstream인 markdown/asciidoc 임베딩 검색 MCP&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://core.telegram.org/bots/webhooks&quot;&gt;Telegram Bot API: Webhooks&lt;/a&gt; — webhook 모드 공식 문서&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Linux 바이너리 역어셈블로 커널 모듈 패치 확인하기</title>
      <link>https://blog.benelog.net//linux-binary-disassembly.html</link>
      <pubDate>Sun, 12 Apr 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">linux-binary-disassembly.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#역어셈블의_기본_개념&quot;&gt;1. 역어셈블의 기본 개념&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#elf과_ko&quot;&gt;1.1. ELF과 .ko&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#핵심_도구&quot;&gt;1.2. 핵심 도구&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#x86_64_호출_규약_system_v_amd64_abi&quot;&gt;1.3. x86-64 호출 규약 (System V AMD64 ABI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#자주_보이는_x86_64_명령어&quot;&gt;1.4. 자주 보이는 x86-64 명령어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#상수로_호출_지점_찾기&quot;&gt;1.5. 상수로 호출 지점 찾기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#활용_예시_커널_모듈_바이너리_비교&quot;&gt;2. 활용 예시: 커널 모듈 바이너리 비교&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#문제_상황&quot;&gt;2.1. 문제 상황&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#왜_github_소스_비교_대신_역어셈블을_선택했나&quot;&gt;2.2. 왜 GitHub 소스 비교 대신 역어셈블을 선택했나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#모듈_추출과_역어셈블&quot;&gt;2.3. 모듈 추출과 역어셈블&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#핵심_차이_찾기&quot;&gt;2.4. 핵심 차이 찾기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#재배치_정보로_call_대상_확인&quot;&gt;2.5. 재배치 정보로 call 대상 확인&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#주의할_점&quot;&gt;3. 주의할 점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;4. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소스 코드를 구할 수 없는 Linux 바이너리를 분석해야 할 때가 있습니다. 이 글에서는 &lt;code&gt;objdump&lt;/code&gt; 기반의 역어셈블 기법을 먼저 정리하고, 활용 예시로 Ubuntu 패키지의 &lt;code&gt;intel_cvs.ko&lt;/code&gt; 커널 모듈과 DKMS 패치 버전을 비교한 사례의 핵심을 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;역어셈블의_기본_개념&quot;&gt;1. 역어셈블의 기본 개념&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;elf과_ko&quot;&gt;1.1. ELF과 .ko&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Linux의 실행 파일과 공유 라이브러리, 커널 모듈은 모두 ELF(Executable and Linkable Format) 포맷입니다. 특히 커널 모듈(&lt;code&gt;.ko&lt;/code&gt;)은 &lt;strong&gt;relocatable object&lt;/strong&gt; 파일로, 커널에 로드되는 시점에 주소가 확정됩니다. 따라서 역어셈블에서 &lt;code&gt;call&lt;/code&gt; 대상이 `0x0000`으로 보이는 것은 비정상이 아니라 재배치가 아직 적용되지 않았기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ELF는 섹션 단위로 구성됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.text&lt;/code&gt; — 코드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.rodata&lt;/code&gt; — 읽기 전용 데이터, 문자열 리터럴&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.data&lt;/code&gt; — 전역 변수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.symtab&lt;/code&gt; — 심볼 테이블&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최근 Ubuntu OEM 커널은 모듈을 zstd로 압축하므로 `zstd -d`로 먼저 해제해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;핵심_도구&quot;&gt;1.2. 핵심 도구&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;도구&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;objdump&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ELF 역어셈블. `-d -M intel`이 가장 많이 쓰는 조합&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;nm&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;심볼 테이블 조회. 함수 주소와 이름 확인&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;readelf&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ELF 헤더, 섹션, 재배치 정보 확인&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;strings&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;바이너리 안의 문자열 추출. 힌트 얻기 용도&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;modinfo&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;커널 모듈 메타데이터&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;pahole&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;DWARF 디버그 정보에서 구조체 레이아웃 추출&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;x86_64_호출_규약_system_v_amd64_abi&quot;&gt;1.3. x86-64 호출 규약 (System V AMD64 ABI)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;역어셈블을 읽기 위해 가장 먼저 알아야 하는 것은 호출 규약입니다. Linux는 System V AMD64 ABI를 따르며, 함수 인자는 정해진 순서의 레지스터로 전달됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;인자 순서&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;레지스터&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;1번째&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;rdi&lt;/code&gt; / &lt;code&gt;edi&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;2번째&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;rsi&lt;/code&gt; / &lt;code&gt;esi&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;3번째&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;rdx&lt;/code&gt; / &lt;code&gt;edx&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;4번째&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;rcx&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;5번째&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;r8&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;6번째&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;r9&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;반환값&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;rax&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들어 다음 시퀀스는 &lt;code&gt;foo(0x805, NULL, 0)&lt;/code&gt; 호출과 정확히 대응됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asm&quot;&gt;xor    edx, edx            ; 3번째 인자: 0
xor    esi, esi            ; 2번째 인자: NULL
mov    edi, 0x805          ; 1번째 인자: 0x805
call   foo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;자주_보이는_x86_64_명령어&quot;&gt;1.4. 자주 보이는 x86-64 명령어&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;명령어&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;mov&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;값 복사. &lt;code&gt;mov edi, 0x805&lt;/code&gt; → &lt;code&gt;edi = 0x805&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;movzx&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;작은 값을 0 확장하여 복사. 1바이트를 32비트로 읽을 때&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;lea&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;주소 계산만 수행 (메모리 접근 없음). 포인터 전달 용도&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;test reg, reg&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;같은 레지스터로 AND. 0인지 검사&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cmp&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;뺄셈 후 플래그만 설정&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;je&lt;/code&gt; / &lt;code&gt;jne&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;조건 점프 (equal / not equal)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;xor reg, reg&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;레지스터를 0으로 초기화 (`mov reg, 0`보다 짧음)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;call&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;함수 호출&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;상수로_호출_지점_찾기&quot;&gt;1.5. 상수로 호출 지점 찾기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소스에서 &lt;code&gt;#define&lt;/code&gt; 값을 알고 있으면 역어셈블에서 해당 상수를 검색해 호출 지점을 좁힐 수 있습니다. 다만 같은 값이 다른 맥락(예: 길이, 인덱스)에서 쓰일 수 있으므로, 그 상수가 &lt;strong&gt;어느 레지스터에 들어가는지&lt;/strong&gt;와 주변 &lt;code&gt;call&lt;/code&gt; 대상을 함께 확인해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;활용_예시_커널_모듈_바이너리_비교&quot;&gt;2. 활용 예시: 커널 모듈 바이너리 비교&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;문제_상황&quot;&gt;2.1. 문제 상황&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ubuntu의 &lt;code&gt;linux-modules-vision-*-oem&lt;/code&gt; 패키지에 포함된 `intel_cvs.ko`가 Intel 업스트림의 특정 버그 픽스를 반영하고 있는지 확인해야 했습니다. 패키지 changelog에는 어느 커밋까지 포함되었는지 명시되어 있지 않고, DKMS로 설치한 패치 버전과 어떻게 다른지를 확인해야 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;왜_github_소스_비교_대신_역어셈블을_선택했나&quot;&gt;2.2. 왜 GitHub 소스 비교 대신 역어셈블을 선택했나&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intel의 업스트림 저장소가 GitHub에 공개되어 있으므로 소스 레벨 `diff`가 더 쉬운 선택처럼 보입니다. 그런데도 역어셈블을 택한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;어떤 커밋에서 빌드되었는지 모른다.&lt;/strong&gt; Ubuntu OEM 패키지의 changelog에는 Intel 업스트림의 어떤 시점 스냅샷인지가 명시되어 있지 않았습니다. 임의의 커밋에서 찍은 소스와 비교하면 &quot;차이&quot;가 진짜 패치 누락 때문인지 단순한 버전 갭 때문인지 구분되지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu가 자체 패치를 얹었을 수 있다.&lt;/strong&gt; 패키지 빌드는 업스트림 tarball에 distro 패치를 덧붙이는 경우가 흔합니다. 업스트림 소스만으로는 실제로 빌드된 결과를 재현할 수 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;바이너리가 ground truth다.&lt;/strong&gt; 커널이 실제로 로드하는 것은 &lt;code&gt;.ko&lt;/code&gt; 바이너리입니다. &quot;이 시스템에서 지금 돌고 있는 코드에 해당 가드가 있는가&quot;라는 질문의 답은 바이너리 안에만 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;확인하려는 범위가 작다.&lt;/strong&gt; 특정 한 가지 패치(특정 상수 호출 앞의 분기 유무)만 확인하면 충분했습니다. 소스 트리 전체를 받아 빌드 환경을 맞추는 수고보다 &lt;code&gt;objdump&lt;/code&gt; 한 번이 더 빠른 상황이었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉, 소스 비교는 &quot;업스트림에 그 패치가 존재하는가&quot;에 답하지만, 이번에 필요한 답은 &quot;내 시스템에 설치된 바이너리에 그 패치가 반영되어 있는가&quot;였고, 이 질문에는 바이너리를 직접 보는 것이 가장 확실한 방법이었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;모듈_추출과_역어셈블&quot;&gt;2.3. 모듈 추출과 역어셈블&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;# 압축 해제
zstd -d -c /lib/modules/$(uname -r)/ubuntu/vision/intel_cvs.ko.zst &amp;gt; /tmp/pkg.ko
zstd -d -c /lib/modules/$(uname -r)/updates/dkms/intel_cvs.ko.zst &amp;gt; /tmp/dkms.ko

# 동일성 확인
md5sum /tmp/pkg.ko /tmp/dkms.ko

# 관심 함수만 Intel 문법으로 역어셈블
objdump -d -M intel --no-show-raw-insn /tmp/pkg.ko \
  | sed -n &apos;/&amp;lt;cvs_common_probe&amp;gt;:/,/^$/p&apos; &amp;gt; /tmp/pkg_probe.asm

objdump -d -M intel --no-show-raw-insn /tmp/dkms.ko \
  | sed -n &apos;/&amp;lt;cvs_common_probe&amp;gt;:/,/^$/p&apos; &amp;gt; /tmp/dkms_probe.asm&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;-M intel`은 `mov dst, src&lt;/code&gt; 순서의 Intel 문법을 사용합니다 (기본은 `mov src, dst`의 AT&amp;amp;T 문법). `--no-show-raw-insn`은 기계어 바이트를 생략해 가독성을 높입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;핵심_차이_찾기&quot;&gt;2.4. 핵심 차이 찾기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;확인하고 싶은 패치는 아래 형태의 가드였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;if (icvs-&amp;gt;magic_num_support) {
    ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;SET_HOST_IDENTIFIER = 0x0805`는 첫 번째 인자이므로 `mov edi, 0x805&lt;/code&gt; 패턴으로 역어셈블에서 검색할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;DKMS 버전 (가드 있음)&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asm&quot;&gt;movzx  eax, BYTE PTR [r12+0x4c]  ; icvs-&amp;gt;magic_num_support 로드
and    ebx, 0x1
test   bl, bl                    ; 0인지 검사
je     12c7                      ; 0이면 호출 건너뜀
xor    edx, edx                  ; len = 0
xor    esi, esi                  ; buf = NULL
mov    edi, 0x805                ; SET_HOST_IDENTIFIER
call   cvs_write_i2c&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;패키지 버전 (가드 없음)&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;asm&quot;&gt;xor    edx, edx
xor    esi, esi
mov    edi, 0x805
call   cvs_write_i2c             ; 무조건 실행됨&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;mov edi, 0x805&lt;/code&gt; 바로 앞에 &lt;code&gt;test&lt;/code&gt; + `je`로 된 분기가 있느냐 없느냐가 두 바이너리의 실질적 차이였습니다. 즉 패키지 버전에는 해당 패치가 반영되어 있지 않다는 결론이 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;재배치_정보로_call_대상_확인&quot;&gt;2.5. 재배치 정보로 call 대상 확인&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;.ko`의 `call&lt;/code&gt; 대상이 &lt;code&gt;0x0000`이거나 어색한 주소로 보일 때는 `-r&lt;/code&gt; 옵션으로 재배치 정보를 함께 출력하면 원래 타깃 심볼을 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;objdump -d -r -M intel /tmp/pkg.ko&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;127f: call   1284 &amp;lt;cvs_common_probe+0x104&amp;gt;
      1280: R_X86_64_PLT32  cvs_write_i2c-0x4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;주의할_점&quot;&gt;3. 주의할 점&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;컴파일러 최적화&lt;/strong&gt;로 같은 소스여도 명령어 순서와 레지스터 선택이 달라집니다. 정확히 같은 시퀀스가 아니라 &lt;strong&gt;논리적 흐름(분기 구조)&lt;/strong&gt; 을 비교해야 합니다. DKMS 빌드와 패키지 빌드의 GCC 버전이 같으면 차이가 적고, 다르면 코드 구조 자체가 꽤 달라 보일 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;재배치 주소 차이는 무시&lt;/strong&gt;해야 합니다. &lt;code&gt;diff`를 돌리면 `je 1553&lt;/code&gt; vs `je 12c7`처럼 점프 대상 주소가 대량으로 다르게 나오는데, 이것은 함수 배치 차이일 뿐이고 주목해야 할 것은 명령어 자체의 추가/삭제입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;상수 검색의 false positive&lt;/strong&gt;: &lt;code&gt;mov edi, 0x805`가 반드시 원하는 호출은 아닐 수 있습니다. 어떤 함수 안에 있는지, 직후의 `call&lt;/code&gt; 대상이 무엇인지로 맥락을 확인해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 당연한 이야기지만, 소스를 구할 수 있다면 역어셈블보다 `diff`가 훨씬 빠르고 정확합니다. 역어셈블은 소스를 확보할 수 없을 때의 최후 수단입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;4. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.egr.unlv.edu/~ed/x86.html&quot;&gt;x86-64 Assembly Language Programming with Ubuntu&lt;/a&gt; — x86-64 어셈블리 입문 교재 (무료 PDF)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.felixcloutier.com/x86/&quot;&gt;Intel x86-64 Instruction Set Reference&lt;/a&gt; — Intel 공식 매뉴얼 웹 버전&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.osdev.org/System_V_ABI&quot;&gt;System V ABI - OSDev Wiki&lt;/a&gt; — 호출 규약 요약&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sourceware.org/binutils/docs/binutils/&quot;&gt;GNU Binutils Documentation&lt;/a&gt; — &lt;code&gt;objdump&lt;/code&gt;, &lt;code&gt;nm&lt;/code&gt;, &lt;code&gt;readelf&lt;/code&gt; 공식 문서&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://refspecs.linuxfoundation.org/elf/elf.pdf&quot;&gt;ELF Format 공식 스펙&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ghidra-sre.org/&quot;&gt;Ghidra&lt;/a&gt; — NSA가 공개한 역공학 도구. `objdump`보다 훨씬 강력한 디컴파일과 크로스 레퍼런스 제공&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://godbolt.org/&quot;&gt;Compiler Explorer (Godbolt)&lt;/a&gt; — C 코드가 어떤 어셈블리로 컴파일되는지 실시간 확인&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code가 정상혁의 지시에 따라서 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Claude Code의 Prompt Cache 버그 (v2.1.69~v2.1.89)</title>
      <link>https://blog.benelog.net//claude-code-cache-bug.html</link>
      <pubDate>Fri, 3 Apr 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">claude-code-cache-bug.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#버그_요약&quot;&gt;1. 버그 요약&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#원인_resume_시_메시지_구조_불일치&quot;&gt;2. 원인: Resume 시 메시지 구조 불일치&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#근본_원인&quot;&gt;2.1. 근본 원인&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mcp_서버_deferred_tools와의_관계&quot;&gt;2.2. MCP 서버, deferred tools와의 관계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#부수적_문제_매_턴_tool_schema_재직렬화&quot;&gt;2.3. 부수적 문제: 매 턴 tool schema 재직렬화&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#영향_범위&quot;&gt;3. 영향 범위&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#비용_영향&quot;&gt;4. 비용 영향&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#수정_내용_v2_1_90&quot;&gt;5. 수정 내용 (v2.1.90)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#영향_받았는지_확인하는_방법&quot;&gt;6. 영향 받았는지 확인하는 방법&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#resume_사용_여부_확인&quot;&gt;6.1. Resume 사용 여부 확인&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#api_사용량_확인&quot;&gt;6.2. API 사용량 확인&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#캐시_메트릭_모니터링&quot;&gt;6.3. 캐시 메트릭 모니터링&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;7. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sidebarblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;title&quot;&gt;변경 이력&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2026-04-06: Prompt Cache TTL 기본값(5분)과 1시간 옵션에 대한 설명 추가&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 3월 초부터 약 한 달간 Claude Code에 Prompt Cache가 제대로 작동하지 않는 버그가 있었습니다.
&lt;a href=&quot;claude-code-cost.html&quot;&gt;Claude Code 토큰 비용과 프롬프트 캐싱&lt;/a&gt;에서 정리했듯이 Prompt Caching은 입력 토큰 비용을 90%까지 절약하는 핵심 수단인데, 이 캐시가 작동하지 않으면 비용이 크게 증가합니다.
이 글에서는 해당 기간에 발생한 캐시 버그의 원인과 영향을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;버그_요약&quot;&gt;1. 버그 요약&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;영향 버전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;v2.1.69 (2026-03-05) ~ v2.1.89&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;수정 버전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;v2.1.90 (2026-04-01)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;근본 원인&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;v2.1.69에서 도입된 &lt;code&gt;deferred_tools_delta`로 인해 resume 시 `messages[0]&lt;/code&gt; 구조가 새 세션과 달라짐&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;영향 대상&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--resume&lt;/code&gt; 또는 `/resume`으로 세션을 재개하는 사용자&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;GitHub Issue&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/34629&quot;&gt;#34629&lt;/a&gt;, &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/28899&quot;&gt;#28899&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;원인_resume_시_메시지_구조_불일치&quot;&gt;2. 원인: Resume 시 메시지 구조 불일치&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;근본_원인&quot;&gt;2.1. 근본 원인&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;v2.1.69에서 도입된 &lt;code&gt;deferred_tools_delta&lt;/code&gt; 기능이 원인입니다.
이 기능은 deferred tools 목록, MCP 서버 설정, skills 정보를 메시지에 첨부하는 방식을 변경했는데, 새 세션과 재개 세션에서 `messages[0]`의 구조가 달라지는 문제가 생겼습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;세션 유형&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;messages[0] 내용&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;크기&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;새 세션&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;deferred tools 목록, MCP 설정, skills, 날짜 컨텍스트&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;~13.4KB&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;재개 세션&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;날짜 컨텍스트만 포함 (나머지는 messages 끝에 추가)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;~352B&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Prompt Cache는 prefix의 해시값으로 캐시를 조회합니다.
`messages[0]`의 내용이 13.4KB에서 352B로 바뀌면 해시값이 완전히 달라지므로, 이전 세션에서 쌓은 캐시를 전혀 재사용할 수 없었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;mcp_서버_deferred_tools와의_관계&quot;&gt;2.2. MCP 서버, deferred tools와의 관계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 버그는 MCP 서버나 deferred tools를 사용하는 환경에서 더 큰 영향을 받았습니다.
다만 이것은 MCP 서버의 tool schema가 불안정해서가 아니라, deferred tools/MCP 설정/skills 정보가 resume 시 `messages[0]`이 아닌 다른 위치에 배치되어 prefix가 달라지기 때문입니다.
MCP 서버가 매번 완전히 동일한 schema를 반환하더라도, resume만 하면 메시지 구조가 달라져서 캐시 미스가 발생했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;v2.1.90의 수정 내용도 이를 뒷받침합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Fixed &lt;code&gt;--resume&lt;/code&gt; causing a full prompt-cache miss on the first request for users with deferred tools, MCP servers, or custom agents&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;부수적_문제_매_턴_tool_schema_재직렬화&quot;&gt;2.3. 부수적 문제: 매 턴 tool schema 재직렬화&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;근본 원인과 별개로, Claude Code가 매 턴마다 MCP tool schema를 `JSON.stringify`로 재계산하여 캐시 키를 만드는 비효율도 있었습니다.
이것은 캐시 미스를 유발하는 직접적인 원인은 아니었지만 성능 오버헤드를 발생시켰고, v2.1.90에서 tool schema를 한 번만 계산하고 재사용하도록 개선되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;영향_범위&quot;&gt;3. 영향 범위&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 버그는 &lt;code&gt;--resume&lt;/code&gt; (CLI 플래그)과 &lt;code&gt;/resume&lt;/code&gt; (슬래시 커맨드) 모두에 해당합니다.
두 방식 모두 세션 재개 시 동일한 메시지 구조 불일치가 발생하기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;단, Prompt Cache의 TTL이 기본 5분 &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_1&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_1&quot; title=&quot;View footnote.&quot;&gt;1&lt;/a&gt;]&lt;/sup&gt; 이므로 실질적인 비용 손해가 발생하는 경우는 제한적입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 66.6667%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;시나리오&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;일반 연속 세션 (resume 없이)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;영향 없음. `messages[0]`이 세션 내내 안정적으로 유지&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;5분 이내에 resume&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시가 살아있어야 하는데 prefix 불일치로 캐시 미스 발생 → &lt;strong&gt;비용 손해&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;5분 이후에 resume&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;어차피 TTL 만료로 캐시 소멸 → 버그와 무관하게 비용 동일&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;--print --resume&lt;/code&gt; 자동화&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;매 호출이 새 프로세스이므로 캐시를 전혀 재활용하지 못함 → &lt;strong&gt;가장 큰 피해&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;비용_영향&quot;&gt;4. 비용 영향&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정상 상태에서는 캐시가 쌓이면서 턴이 진행될수록 메시지당 비용이 감소합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;정상 (v2.1.68):  메시지 1 → $0.15,  메시지 4 → $0.02 (캐시 적중률 증가)
버그 (v2.1.69+): 모든 메시지 → $0.35~0.40 (매번 전체 히스토리를 Cache Write)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--print --resume&lt;/code&gt; 워크플로우에서는 &lt;code&gt;cache_read&lt;/code&gt; 토큰이 ~11,000(시스템 프롬프트 분량)에 머물고, &lt;code&gt;cache_creation&lt;/code&gt; 토큰이 300,000 이상으로 치솟는 현상이 관찰되었습니다.
메시지당 약 &lt;strong&gt;20배의 비용 증가&lt;/strong&gt;가 보고되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;수정_내용_v2_1_90&quot;&gt;5. 수정 내용 (v2.1.90)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;v2.1.90 (2026-04-01)에서 다음과 같이 수정되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;메시지 구조 정규화&lt;/strong&gt;: 세션 재개 시 `messages[0]`에 deferred tools, MCP 설정, skills를 새 세션과 동일한 위치에 삽입하도록 변경&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;tool schema 재직렬화 제거&lt;/strong&gt;: MCP tool schema를 한 번만 계산하고 캐시하여, 매 턴 `JSON.stringify`를 호출하지 않도록 성능 개선&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 버전 확인:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;claude --version&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;v2.1.90 이상이면 수정된 상태입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;영향_받았는지_확인하는_방법&quot;&gt;6. 영향 받았는지 확인하는 방법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 3월 5일~4월 1일 사이에 Claude Code를 사용했다면, 다음 방법으로 영향 여부를 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;resume_사용_여부_확인&quot;&gt;6.1. Resume 사용 여부 확인&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 버그는 resume 시에만 발생하므로, 해당 기간에 resume을 사용했는지가 핵심입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;resume_슬래시_커맨드_확인&quot;&gt;6.1.1. /resume (슬래시 커맨드) 확인&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대화형 세션 안에서 `/resume`을 입력한 기록은 `~/.claude/history.jsonl`에 남습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Claude Code 프롬프트&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;~/.claude/history.jsonl에서 2026년 3월 5일~4월 1일 사이의 /resume 기록을 찾아줘.
각 resume 시점에 대해, 같은 프로젝트의 ~/.claude/projects/ 아래 JSONL 세션 로그에서
직전 세션의 마지막 timestamp와의 시간 간격을 계산해줘.
5분 이내에 resume한 경우가 있는지 확인하고 싶어.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;resume_cli_플래그_확인&quot;&gt;6.1.2. --resume (CLI 플래그) 확인&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`claude --resume`처럼 CLI 인자로 실행한 경우는 `history.jsonl`에 기록되지 않습니다.
이 경우는 shell history에서 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;bash 사용자&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;grep &quot;claude.*--resume&quot; ~/.bash_history&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;zsh 사용자&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;grep &quot;claude.*--resume&quot; ~/.zsh_history&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;bash는 기본적으로 명령 실행 시각을 기록하지 않으므로 &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_2&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_2&quot; title=&quot;View footnote.&quot;&gt;2&lt;/a&gt;]&lt;/sup&gt;, &lt;code&gt;--resume&lt;/code&gt; 사용 여부만 확인할 수 있고 5분 간격 분석은 어렵습니다.
zsh는 &lt;code&gt;EXTENDED_HISTORY&lt;/code&gt; 옵션이 켜져 있으면 `~/.zsh_history`에 타임스탬프가 함께 기록됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--resume`을 주로 자동화 스크립트(&lt;/code&gt;--print --resume`)에서 사용했다면 해당 스크립트의 실행 로그를 확인하는 것이 더 정확합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;5분 이내에 resume한 기록이 없다면, Prompt Cache의 TTL(5분)이 만료된 후 재개한 것이므로 이 버그로 인한 추가 비용은 발생하지 않은 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;api_사용량_확인&quot;&gt;6.2. API 사용량 확인&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Anthropic Console에서 해당 기간의 토큰 사용량을 확인합니다.
평소 대비 `cache_creation_input_tokens`가 비정상적으로 높고 `cache_read_input_tokens`가 낮다면 이 버그의 영향을 받았을 가능성이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;캐시_메트릭_모니터링&quot;&gt;6.3. 캐시 메트릭 모니터링&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세션 중 API 응답의 캐시 관련 토큰을 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cache_creation_input_tokens&lt;/code&gt;: 대화가 진행될수록 &lt;strong&gt;감소&lt;/strong&gt;해야 정상&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cache_read_input_tokens&lt;/code&gt;: 대화가 진행될수록 &lt;strong&gt;증가&lt;/strong&gt;해야 정상&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버그 발생 시에는 `cache_read_input_tokens`가 낮은 값에 머물고, `cache_creation_input_tokens`가 계속 높게 유지됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;7. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/34629&quot;&gt;Prompt cache regression in --print --resume since v2.1.69 - GitHub Issue #34629&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/28899&quot;&gt;Prompt caching not working - GitHub Issue #28899&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://piunikaweb.com/2026/03/31/claude-cache-bugs-tokens-20x-more-anthropic-investigating/&quot;&gt;Anthropic Looking Into Cache Bugs - PiunikaWeb&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;footnotes&quot;&gt;
&lt;hr&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_1&quot;&gt;
&lt;a href=&quot;#_footnoteref_1&quot;&gt;1&lt;/a&gt;. Anthropic API는 기본 5분(&lt;code&gt;ephemeral&lt;/code&gt;)과 1시간(&lt;code&gt;&quot;ttl&quot;: &quot;1h&quot;&lt;/code&gt;) 두 가지 TTL 옵션을 제공합니다. 1시간 옵션은 cache write 비용이 기본 입력 토큰의 2배(5분은 1.25배)로 더 높습니다. Claude Code에서는 구독 플랜에 따라 자동 결정되며(Max 플랜: 1시간, Pro 플랜/API 키: 5분), 사용자가 직접 설정할 수는 없습니다. 자세한 내용은 &lt;a href=&quot;https://platform.claude.com/docs/en/build-with-claude/prompt-caching&quot;&gt;Anthropic 공식 문서&lt;/a&gt;를 참고하세요.
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_2&quot;&gt;
&lt;a href=&quot;#_footnoteref_2&quot;&gt;2&lt;/a&gt;. &lt;code&gt;HISTTIMEFORMAT`을 설정하면 `history&lt;/code&gt; 명령에서 시각이 표시되지만, &lt;code&gt;~/.bash_history&lt;/code&gt; 파일 자체에는 타임스탬프가 저장되지 않습니다.
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Claude Code 비용 최적화를 위한 지표 수집</title>
      <link>https://blog.benelog.net//claude-code-optimize.html</link>
      <pubDate>Wed, 1 Apr 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">claude-code-optimize.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#수집해야_할_지표&quot;&gt;1. 수집해야 할 지표&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#컨텍스트_관리_효율성&quot;&gt;1.1. 컨텍스트 관리 효율성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#도구_사용_패턴&quot;&gt;1.2. 도구 사용 패턴&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#세션_수명_주기&quot;&gt;1.3. 세션 수명 주기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#api_호출과_비용&quot;&gt;1.4. API 호출과 비용&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#지표별_수집_방법&quot;&gt;2. 지표별 수집 방법&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#hook으로_수집하는_지표&quot;&gt;2.1. Hook으로 수집하는 지표&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#opentelemetry로_수집하는_지표&quot;&gt;2.2. OpenTelemetry로 수집하는 지표&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#수집_방법_요약&quot;&gt;2.3. 수집 방법 요약&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#개인_분석_로컬_로그로_셀프_리포트_만들기&quot;&gt;3. 개인 분석: 로컬 로그로 셀프 리포트 만들기&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#분석_스크립트_예시&quot;&gt;3.1. 분석 스크립트 예시&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#수집_데이터_저장소&quot;&gt;4. 수집 데이터 저장소&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#저장소_구성&quot;&gt;4.1. 저장소 구성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#저장소_선택지&quot;&gt;4.2. 저장소 선택지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#hook_데이터_중앙_수집&quot;&gt;4.3. Hook 데이터 중앙 수집&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#권고_가이드_생성_로직&quot;&gt;5. 권고 가이드 생성 로직&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#컨텍스트_관리_권고&quot;&gt;5.1. 컨텍스트 관리 권고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#도구_사용_권고&quot;&gt;5.2. 도구 사용 권고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#모델_선택_권고&quot;&gt;5.3. 모델 선택 권고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#비용_효율_권고&quot;&gt;5.4. 비용 효율 권고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#리포트_예시&quot;&gt;5.5. 리포트 예시&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#주의할_점&quot;&gt;6. 주의할 점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;7. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code를 효율적으로 사용하고 있는지 개인적으로나 팀 차원에서 자주 돌아보고 개선 시도를 한다면, 같은 작업을 더 적은 비용으로 할 수 있습니다.
이 글에서는 Claude Code의 Hook과 OpenTelemetry를 활용하여 사용 지표를 수집하고, 수집된 데이터로 개인별 개선 권고 리포트를 만드는 방법을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;수집해야_할_지표&quot;&gt;1. 수집해야 할 지표&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;비용 최적화 관점에서 의미 있는 지표는 크게 네 영역으로 나뉩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;컨텍스트_관리_효율성&quot;&gt;1.1. 컨텍스트 관리 효율성&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컨텍스트가 가득 차서 자동 압축(autocompact)이 발생하면, 그만큼 한 세션에서 많은 토큰을 소비했다는 신호입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;지표&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 시작 유형 (&lt;code&gt;source&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;startup&lt;/code&gt;(신규), &lt;code&gt;resume&lt;/code&gt;(재개), &lt;code&gt;compact&lt;/code&gt;(압축 후), &lt;code&gt;clear&lt;/code&gt;(&lt;code&gt;/clear&lt;/code&gt; 후) 비율로 사용 습관을 파악&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;자동 압축 빈도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션당 autocompact 횟수가 많으면 작업 단위를 더 작게 분리할 필요가 있음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;압축 간격&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;압축 사이 시간이 짧을수록 컨텍스트를 빠르게 소진하고 있다는 의미&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;도구_사용_패턴&quot;&gt;1.2. 도구 사용 패턴&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code는 파일 읽기, 편집, 명령 실행 등 도구를 호출할 때마다 API 호출이 발생합니다.
도구 사용 방식에 따라 같은 작업의 토큰 소비량이 크게 달라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;지표&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도구별 호출 빈도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;어떤 도구를 주로 쓰는지 파악. &lt;code&gt;Bash`로 `cat&lt;/code&gt;, &lt;code&gt;grep`을 반복 호출하면 전용 도구(`Read&lt;/code&gt;, &lt;code&gt;Grep&lt;/code&gt;)보다 토큰을 더 소비&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도구 호출 실패율&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실패 후 재시도는 토큰 낭비. 반복 실패 패턴은 사용자 교육이 필요한 영역&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서브에이전트 생성 빈도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;Agent&lt;/code&gt; 도구로 서브에이전트를 생성하면 추가 API 호출이 발생. 불필요한 서브에이전트 생성은 비용 증가 요인&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;세션_수명_주기&quot;&gt;1.3. 세션 수명 주기&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;지표&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 길이 (시작~종료 시간)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션이 길수록 컨텍스트가 누적되어 턴당 입력 토큰이 증가 &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_1&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_1&quot; title=&quot;View footnote.&quot;&gt;1&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 종료 사유&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;정상 종료, 비정상 종료, rate limit 등의 비율&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;api_호출과_비용&quot;&gt;1.4. API 호출과 비용&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 직접적인 비용 지표입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;지표&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;API 호출 횟수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션당, 프롬프트당 API 호출 수&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;호출당 토큰 수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;입력/출력/캐시 읽기/캐시 쓰기 토큰&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;호출당 비용 (USD)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모델별 단가 적용된 실제 비용&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모델별 사용 비율&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Opus vs Sonnet vs Haiku. Opus는 Sonnet 대비 5배 비용&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시 히트율&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시 읽기 토큰 비율이 높을수록 비용 효율적&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;지표별_수집_방법&quot;&gt;2. 지표별 수집 방법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;수집 방법은 크게 두 가지입니다. &lt;strong&gt;Hook&lt;/strong&gt;은 사용 행동 패턴을, &lt;strong&gt;OpenTelemetry&lt;/strong&gt;는 정확한 비용 데이터를 수집합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;hook으로_수집하는_지표&quot;&gt;2.1. Hook으로 수집하는 지표&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code의 &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;Hook&lt;/a&gt;은 특정 이벤트 발생 시 셸 스크립트를 실행하는 기능입니다. 비용 데이터에는 직접 접근할 수 없지만, 사용 행동 패턴을 기록하는 데 적합합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Hook 이벤트&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;수집 가능한 지표&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;SessionStart&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 시작 유형(&lt;code&gt;source&lt;/code&gt;), 사용 모델(&lt;code&gt;model&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;source&lt;/code&gt; 값: &lt;code&gt;startup&lt;/code&gt;, &lt;code&gt;resume&lt;/code&gt;, &lt;code&gt;compact&lt;/code&gt;, &lt;code&gt;clear&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;SessionEnd&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 종료 시각, 종료 사유&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 시작 시각과 조합하면 세션 길이 산출&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;PreCompact&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;압축 발생 시각, 자동/수동 여부&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;`matcher: auto`로 자동 압축만 필터링 가능&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;PreToolUse&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도구 이름, 호출 시각&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;고빈도 이벤트이므로 성능 영향 고려 필요&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;PostToolUseFailure&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실패한 도구, 에러 내용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;반복 실패 패턴 분석용&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;SubagentStart&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서브에이전트 생성 시각&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;불필요한 서브에이전트 생성 패턴 분석&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;수집_스크립트_예시&quot;&gt;2.1.1. 수집 스크립트 예시&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;하나의 스크립트에서 이벤트 유형별로 분기하여 JSONL 형식으로 기록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.claude/scripts/log-usage.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/bin/bash
INPUT=$(cat)
EVENT=$(echo &quot;$INPUT&quot; | jq -r &apos;.hook_event_name&apos;)
SESSION=$(echo &quot;$INPUT&quot; | jq -r &apos;.session_id&apos;)
TIMESTAMP=$(date -u &apos;+%Y-%m-%dT%H:%M:%SZ&apos;)
USER=$(whoami)

LOG_DIR=&quot;$HOME/.claude/usage-logs&quot;
mkdir -p &quot;$LOG_DIR&quot;

case &quot;$EVENT&quot; in
  SessionStart)
    SOURCE=$(echo &quot;$INPUT&quot; | jq -r &apos;.source&apos;)
    MODEL=$(echo &quot;$INPUT&quot; | jq -r &apos;.model&apos;)
    echo &quot;{\&quot;ts\&quot;:\&quot;$TIMESTAMP\&quot;,\&quot;user\&quot;:\&quot;$USER\&quot;,\&quot;event\&quot;:\&quot;session_start\&quot;,\&quot;session\&quot;:\&quot;$SESSION\&quot;,\&quot;source\&quot;:\&quot;$SOURCE\&quot;,\&quot;model\&quot;:\&quot;$MODEL\&quot;}&quot; \
      &amp;gt;&amp;gt; &quot;$LOG_DIR/events.jsonl&quot;
    ;;
  SessionEnd)
    echo &quot;{\&quot;ts\&quot;:\&quot;$TIMESTAMP\&quot;,\&quot;user\&quot;:\&quot;$USER\&quot;,\&quot;event\&quot;:\&quot;session_end\&quot;,\&quot;session\&quot;:\&quot;$SESSION\&quot;}&quot; \
      &amp;gt;&amp;gt; &quot;$LOG_DIR/events.jsonl&quot;
    ;;
  PreCompact)
    echo &quot;{\&quot;ts\&quot;:\&quot;$TIMESTAMP\&quot;,\&quot;user\&quot;:\&quot;$USER\&quot;,\&quot;event\&quot;:\&quot;compact\&quot;,\&quot;session\&quot;:\&quot;$SESSION\&quot;}&quot; \
      &amp;gt;&amp;gt; &quot;$LOG_DIR/events.jsonl&quot;
    ;;
  PreToolUse)
    TOOL=$(echo &quot;$INPUT&quot; | jq -r &apos;.tool_name&apos;)
    echo &quot;{\&quot;ts\&quot;:\&quot;$TIMESTAMP\&quot;,\&quot;user\&quot;:\&quot;$USER\&quot;,\&quot;event\&quot;:\&quot;tool_use\&quot;,\&quot;session\&quot;:\&quot;$SESSION\&quot;,\&quot;tool\&quot;:\&quot;$TOOL\&quot;}&quot; \
      &amp;gt;&amp;gt; &quot;$LOG_DIR/events.jsonl&quot;
    ;;
  PostToolUseFailure)
    TOOL=$(echo &quot;$INPUT&quot; | jq -r &apos;.tool_name&apos;)
    echo &quot;{\&quot;ts\&quot;:\&quot;$TIMESTAMP\&quot;,\&quot;user\&quot;:\&quot;$USER\&quot;,\&quot;event\&quot;:\&quot;tool_fail\&quot;,\&quot;session\&quot;:\&quot;$SESSION\&quot;,\&quot;tool\&quot;:\&quot;$TOOL\&quot;}&quot; \
      &amp;gt;&amp;gt; &quot;$LOG_DIR/events.jsonl&quot;
    ;;
  SubagentStart)
    echo &quot;{\&quot;ts\&quot;:\&quot;$TIMESTAMP\&quot;,\&quot;user\&quot;:\&quot;$USER\&quot;,\&quot;event\&quot;:\&quot;subagent\&quot;,\&quot;session\&quot;:\&quot;$SESSION\&quot;}&quot; \
      &amp;gt;&amp;gt; &quot;$LOG_DIR/events.jsonl&quot;
    ;;
esac&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;settings_json_설정&quot;&gt;2.1.2. settings.json 설정&lt;/h4&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.claude/settings.json&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;hooks&quot;: {
    &quot;SessionStart&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;~/.claude/scripts/log-usage.sh&quot; }] }
    ],
    &quot;SessionEnd&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;~/.claude/scripts/log-usage.sh&quot; }] }
    ],
    &quot;PreCompact&quot;: [
      { &quot;matcher&quot;: &quot;auto&quot;,
        &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;~/.claude/scripts/log-usage.sh&quot; }] }
    ],
    &quot;PreToolUse&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;~/.claude/scripts/log-usage.sh&quot; }] }
    ],
    &quot;PostToolUseFailure&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;~/.claude/scripts/log-usage.sh&quot; }] }
    ],
    &quot;SubagentStart&quot;: [
      { &quot;hooks&quot;: [{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;~/.claude/scripts/log-usage.sh&quot; }] }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;opentelemetry로_수집하는_지표&quot;&gt;2.2. OpenTelemetry로 수집하는 지표&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hook에서는 API 호출 횟수, 토큰 수, 비용 같은 핵심 비용 데이터에 접근할 수 없습니다.
이 데이터는 Claude Code의 OpenTelemetry 연동을 통해 수집할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code는 &lt;code&gt;claude_code.api_request&lt;/code&gt; 이벤트를 OTLP로 내보냅니다. 이 이벤트에는 다음 필드가 포함됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 66.6667%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;필드&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;model&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용된 모델 (예: &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cost_usd&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;해당 API 호출의 비용 (USD)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;input_tokens&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;입력 토큰 수&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;output_tokens&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;출력 토큰 수&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cache_read_tokens&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시에서 읽은 토큰 수&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;cache_creation_tokens&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시에 새로 쓴 토큰 수&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;duration_ms&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;API 응답 시간&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;event.sequence&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 내 API 호출 순번 (순서 보장용)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;prompt.id&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용자 프롬프트 단위 UUID (한 프롬프트에서 여러 API 호출이 발생할 수 있음)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;활성화_방법&quot;&gt;2.2.1. 활성화 방법&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;환경 변수를 설정하면 Claude Code가 OTLP 프로토콜로 텔레메트리를 전송합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개발자 PC의 셸 프로파일(&lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&gt; 등)에 추가하거나, 전사 dotfiles 관리 도구로 배포할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;수집_방법_요약&quot;&gt;2.3. 수집 방법 요약&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;지표&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Hook&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;OpenTelemetry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 시작/종료, 압축 빈도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도구 사용 패턴, 실패율&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;API 호출 횟수&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;토큰 수 (입력/출력/캐시)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;호출당 비용 (USD)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모델별 사용 비율&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;개인_분석_로컬_로그로_셀프_리포트_만들기&quot;&gt;3. 개인 분석: 로컬 로그로 셀프 리포트 만들기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전사 수집 인프라가 없더라도, 앞서 설명한 Hook 스크립트로 로컬에 쌓인 &lt;code&gt;~/.claude/usage-logs/events.jsonl&lt;/code&gt; 파일만으로 개인 사용 패턴을 분석할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;분석_스크립트_예시&quot;&gt;3.1. 분석 스크립트 예시&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`jq`와 셸 명령만으로 주요 지표를 산출하는 스크립트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.claude/scripts/self-report.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/bin/bash
LOG=&quot;$HOME/.claude/usage-logs/events.jsonl&quot;

if [ ! -f &quot;$LOG&quot; ]; then
  echo &quot;로그 파일이 없습니다: $LOG&quot;
  exit 1
fi

# 분석 기간 (기본: 최근 7일)
SINCE=$(date -u -d &apos;7 days ago&apos; &apos;+%Y-%m-%dT00:00:00Z&apos; 2&amp;gt;/dev/null \
     || date -u -v-7d &apos;+%Y-%m-%dT00:00:00Z&apos;)  # macOS 호환
DATA=$(jq -c --arg since &quot;$SINCE&quot; &apos;select(.ts &amp;gt;= $since)&apos; &quot;$LOG&quot;)

echo &quot;=== 셀프 사용 리포트 (최근 7일) ===&quot;
echo &quot;&quot;

# 세션 수
SESSIONS=$(echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;session_start&quot;) | .session&apos; | sort -u | wc -l)
echo &quot;[세션 요약]&quot;
echo &quot;  세션 수: $SESSIONS&quot;

# 세션 시작 유형 비율
echo &quot;  시작 유형:&quot;
echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;session_start&quot;) | .source&apos; \
  | sort | uniq -c | sort -rn | while read count source; do
    echo &quot;    $source: $count&quot;
  done

# autocompact 횟수
COMPACTS=$(echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;compact&quot;)&apos; | wc -l)
echo &quot;  자동 압축 횟수: $COMPACTS (세션당 평균: $(echo &quot;scale=1; $COMPACTS / $SESSIONS&quot; | bc 2&amp;gt;/dev/null || echo &quot;N/A&quot;))&quot;
echo &quot;&quot;

# 도구 사용 패턴
echo &quot;[도구 사용]&quot;
TOTAL_TOOLS=$(echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;tool_use&quot;)&apos; | wc -l)
echo &quot;  총 도구 호출: $TOTAL_TOOLS&quot;
echo &quot;  상위 도구:&quot;
echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;tool_use&quot;) | .tool&apos; \
  | sort | uniq -c | sort -rn | head -10 | while read count tool; do
    pct=$(echo &quot;scale=0; $count * 100 / $TOTAL_TOOLS&quot; | bc 2&amp;gt;/dev/null || echo &quot;?&quot;)
    echo &quot;    $tool: $count (${pct}%)&quot;
  done

# Bash로 cat/grep/find 호출 비율 (도구 이름만으로는 Bash 내부 명령을 알 수 없으므로 Bash 비율만 표시)
BASH_COUNT=$(echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;tool_use&quot; and .tool==&quot;Bash&quot;)&apos; | wc -l)
if [ &quot;$TOTAL_TOOLS&quot; -gt 0 ]; then
  BASH_PCT=$(echo &quot;scale=0; $BASH_COUNT * 100 / $TOTAL_TOOLS&quot; | bc 2&amp;gt;/dev/null || echo &quot;?&quot;)
  echo &quot;  Bash 도구 비율: ${BASH_PCT}% (30% 이상이면 전용 도구 활용 권장)&quot;
fi

# 실패율
FAILS=$(echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;tool_fail&quot;)&apos; | wc -l)
if [ &quot;$TOTAL_TOOLS&quot; -gt 0 ]; then
  FAIL_PCT=$(echo &quot;scale=1; $FAILS * 100 / $TOTAL_TOOLS&quot; | bc 2&amp;gt;/dev/null || echo &quot;?&quot;)
  echo &quot;  도구 호출 실패: $FAILS (실패율: ${FAIL_PCT}%)&quot;
fi

# 서브에이전트
SUBAGENTS=$(echo &quot;$DATA&quot; | jq -r &apos;select(.event==&quot;subagent&quot;)&apos; | wc -l)
echo &quot;  서브에이전트 생성: $SUBAGENTS (세션당 평균: $(echo &quot;scale=1; $SUBAGENTS / $SESSIONS&quot; | bc 2&amp;gt;/dev/null || echo &quot;N/A&quot;))&quot;
echo &quot;&quot;

# 간단한 권고
echo &quot;[자동 권고]&quot;
COMPACT_AVG=$(echo &quot;scale=1; $COMPACTS / $SESSIONS&quot; | bc 2&amp;gt;/dev/null || echo &quot;0&quot;)
if [ &quot;$(echo &quot;$COMPACT_AVG &amp;gt;= 3&quot; | bc 2&amp;gt;/dev/null)&quot; = &quot;1&quot; ]; then
  echo &quot;  ⚠ 세션당 autocompact가 ${COMPACT_AVG}회입니다. 작업을 더 작은 단위로 분리하세요.&quot;
fi
if [ &quot;$(echo &quot;$BASH_PCT &amp;gt;= 30&quot; | bc 2&amp;gt;/dev/null)&quot; = &quot;1&quot; ]; then
  echo &quot;  ⚠ Bash 도구 비율이 ${BASH_PCT}%입니다. Read, Grep, Glob 전용 도구를 활용하세요.&quot;
fi
if [ &quot;$(echo &quot;$FAIL_PCT &amp;gt; 15&quot; | bc 2&amp;gt;/dev/null)&quot; = &quot;1&quot; ]; then
  echo &quot;  ⚠ 도구 호출 실패율이 ${FAIL_PCT}%입니다. 반복 실패하는 도구의 사용법을 점검하세요.&quot;
fi
SUBAGENT_AVG=$(echo &quot;scale=1; $SUBAGENTS / $SESSIONS&quot; | bc 2&amp;gt;/dev/null || echo &quot;0&quot;)
if [ &quot;$(echo &quot;$SUBAGENT_AVG &amp;gt; 5&quot; | bc 2&amp;gt;/dev/null)&quot; = &quot;1&quot; ]; then
  echo &quot;  ⚠ 세션당 서브에이전트가 평균 ${SUBAGENT_AVG}회입니다. 간단한 검색은 직접 도구를 호출하세요.&quot;
fi
echo &quot;  (비용 관련 권고는 OpenTelemetry 데이터가 필요합니다)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스크립트 실행 결과 예시:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;=== 셀프 사용 리포트 (최근 7일) ===

[세션 요약]
  세션 수: 15
  시작 유형:
    startup: 10
    resume: 3
    compact: 2
  자동 압축 횟수: 5 (세션당 평균: 0.3)

[도구 사용]
  총 도구 호출: 312
  상위 도구:
    Read: 89 (28%)
    Edit: 67 (21%)
    Bash: 58 (18%)
    Grep: 45 (14%)
    Glob: 32 (10%)
    Write: 12 (3%)
    Agent: 9 (2%)
  Bash 도구 비율: 18% (30% 이상이면 전용 도구 활용 권장)
  도구 호출 실패: 8 (실패율: 2.5%)
  서브에이전트 생성: 9 (세션당 평균: 0.6)

[자동 권고]
  (비용 관련 권고는 OpenTelemetry 데이터가 필요합니다)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 스크립트는 별도의 인프라 없이도 개인이 즉시 실행하여 자신의 사용 습관을 점검할 수 있습니다. 팀 평균과의 비교가 필요 없는 개인 수준의 개선에는 이것만으로 충분합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;수집_데이터_저장소&quot;&gt;4. 수집 데이터 저장소&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다수 사용자의 데이터를 분석하려면 중앙 저장소가 필요합니다. 조직 규모와 기존 인프라에 따라 선택지가 달라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;저장소_구성&quot;&gt;4.1. 저장소 구성&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;개발자 PC                         중앙 수집                    분석/리포트
┌──────────────┐                ┌─────────────────┐         ┌──────────────┐
│ Claude Code  │                │                 │         │              │
│  ├ Hook      │── JSONL 전송 ─▶│  저장소          │────────▶│  분석 파이프  │
│  └ OTel      │── OTLP ──────▶│                 │         │  라인        │
└──────────────┘                └─────────────────┘         └──────────────┘&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;저장소_선택지&quot;&gt;4.2. 저장소 선택지&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 16.6666%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 16.6668%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;방식&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;장점&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;단점&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;적합한 규모&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;파일 기반 (JSONL + S3)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;구축 비용 최소, 별도 인프라 불필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실시간 쿼리 어려움, 분석 시 별도 로딩 필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;소규모 (10명 이하)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OpenTelemetry Collector + Clickhouse&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OTLP 네이티브 수집, 대량 시계열 데이터에 강점&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Clickhouse 운영 부담&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;중규모 (10~100명)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Grafana + Loki/Tempo&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기존 모니터링 인프라 활용 가능, 대시보드 내장&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;학습 곡선, 커스텀 리포트 생성이 번거로움&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모니터링 인프라가 이미 있는 조직&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;BigQuery / Athena&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SQL로 자유로운 분석, 대규모 데이터 처리&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;클라우드 비용, 초기 파이프라인 구축&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;대규모 (100명 이상)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;hook_데이터_중앙_수집&quot;&gt;4.3. Hook 데이터 중앙 수집&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hook은 기본적으로 개발자 PC의 로컬 파일에 기록합니다. 중앙으로 모으려면 추가 처리가 필요합니다. 세 가지 방법이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;cron + rsync/scp&lt;/strong&gt;: 주기적으로 로컬 JSONL을 중앙 서버나 S3로 전송&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hook 스크립트에서 직접 전송&lt;/strong&gt;: `curl`로 수집 API에 POST (단, Hook 실행 시간이 세션 성능에 영향을 줄 수 있으므로 백그라운드 전송 권장)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fluentd/Fluent Bit&lt;/strong&gt;: 로컬 JSONL 파일을 tail하여 중앙 저장소로 전송&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OpenTelemetry 데이터는 OTLP 엔드포인트를 중앙 Collector로 지정하면 별도 처리 없이 수집됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;권고_가이드_생성_로직&quot;&gt;5. 권고 가이드 생성 로직&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;수집된 데이터를 분석하여 개인별 개선 권고를 생성하는 로직입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;컨텍스트_관리_권고&quot;&gt;5.1. 컨텍스트 관리 권고&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;조건&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;판정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션당 autocompact &amp;gt;= 3회&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;컨텍스트 과다 사용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;작업을 더 작은 단위로 분리하세요. 한 세션에서 하나의 목적만 달성하는 것이 비용 효율적입니다.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;compact 간격 &amp;lt; 10분&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;빠른 컨텍스트 소진&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;프롬프트를 더 간결하게 작성하거나, 불필요한 파일 읽기를 줄여보세요.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;source=resume&lt;/code&gt; 비율 &amp;lt; 10%&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션 재개를 활용하지 않음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;이전 세션을 재개(&lt;code&gt;claude --resume&lt;/code&gt;)하면 컨텍스트를 처음부터 다시 구성하지 않아도 됩니다.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;도구_사용_권고&quot;&gt;5.2. 도구 사용 권고&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;조건&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;판정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Bash로 &lt;code&gt;cat&lt;/code&gt;/&lt;code&gt;grep&lt;/code&gt;/&lt;code&gt;find&lt;/code&gt; 호출 비율 &amp;gt; 30%&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;전용 도구 미활용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;Bash 대신 Read, Grep, Glob 등 전용 도구를 사용하면 토큰을 절약할 수 있습니다.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;도구 호출 실패율 &amp;gt; 15%&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;비효율적 도구 사용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;도구 호출 실패가 잦습니다. 에러 메시지를 확인하고 사용법을 점검해보세요.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;세션당 서브에이전트 &amp;gt; 5회&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서브에이전트 남용 가능성&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;서브에이전트 생성을 줄이고, 간단한 검색은 직접 도구를 호출하는 것이 비용 효율적입니다.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;모델_선택_권고&quot;&gt;5.3. 모델 선택 권고&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;조건&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;판정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Opus 사용 비율 &amp;gt; 50%&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;고비용 모델 과다 사용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;단순 편집이나 검색 작업에는 Sonnet을 사용하세요. Opus는 Sonnet 대비 5배 비용입니다.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서브에이전트에서 Opus 사용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서브에이전트 비용 과다&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;서브에이전트는 Sonnet이나 Haiku로 충분한 경우가 많습니다.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;비용_효율_권고&quot;&gt;5.4. 비용 효율 권고&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 항목은 OpenTelemetry 데이터가 있어야 산출 가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 33.3334%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;조건&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;판정&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;권고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시 히트율 &amp;lt; 50%&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시 활용 부족&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;세션 중간에 모델을 전환하거나 CLAUDE.md를 수정하면 캐시가 무효화됩니다. 한 세션에서는 하나의 모델을 유지하세요.&quot; &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_2&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_2&quot; title=&quot;View footnote.&quot;&gt;2&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;프롬프트당 API 호출 &amp;gt; 20회&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;한 프롬프트에 과도한 작업 요청&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;하나의 프롬프트에 여러 작업을 한꺼번에 요청하면 API 호출이 급증합니다. 작업을 나누어 요청하세요.&quot;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;일일 비용이 팀 평균의 2배 이상&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;비용 이상치&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;개별 상담 대상. 사용 패턴 상세 분석 필요&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;리포트_예시&quot;&gt;5.5. 리포트 예시&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 로직을 조합하면 다음과 같은 개인별 주간 리포트를 생성할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;=== 주간 사용 리포트 (2026-03-24 ~ 2026-03-30) ===
사용자: hong

[요약]
  세션 수: 23
  총 API 호출: 847
  총 비용: $42.30
  주요 모델: Sonnet 68%, Opus 32%

[개선 권고]
  1. 컨텍스트 관리
     - 세션당 평균 autocompact: 3.2회 (팀 평균: 1.1회)
     → 작업을 더 작은 단위로 분리하세요.

  2. 도구 사용
     - Bash로 grep/cat 호출: 전체 도구 사용의 41%
     → Read, Grep 전용 도구를 직접 사용하면 토큰을 절약할 수 있습니다.

  3. 모델 선택
     - Opus 사용 비율: 32% (팀 평균: 15%)
     → 단순 작업에서는 Sonnet으로 전환을 권장합니다.
     → 예상 절감: 주당 ~$8.50

[잘하고 있는 점]
  - 캐시 히트율 87% (팀 평균: 72%)
  - 도구 호출 실패율 3% (팀 평균: 8%)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;주의할_점&quot;&gt;6. 주의할 점&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PreToolUse는 고빈도 이벤트&lt;/strong&gt;입니다. Hook 스크립트의 실행 시간이 길면 세션 응답 속도에 영향을 줄 수 있습니다. 스크립트를 가볍게 유지하거나, 샘플링을 적용하거나, 백그라운드로 기록하는 방식을 고려해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;전사 배포가 필요&lt;/strong&gt;합니다. Hook 설정과 환경 변수를 각 개발자 PC에 배포해야 합니다. Enterprise managed config, dotfiles 저장소, 또는 온보딩 스크립트를 활용할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hook으로는 정확한 토큰 수와 비용을 알 수 없습니다&lt;/strong&gt;. 비용 분석이 목적이라면 OpenTelemetry는 필수입니다. Hook만으로는 사용 행동 패턴 분석에 한정됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;개인정보 고려&lt;/strong&gt;가 필요합니다. 프롬프트 내용이나 코드를 수집하는 것이 아니라 메타데이터(이벤트 유형, 도구 이름, 타임스탬프)만 수집하지만, 수집 목적과 범위를 사전에 공유하고 동의를 구하는 것이 바람직합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;7. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;Claude Code Hooks - Anthropic Docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/monitoring-usage&quot;&gt;Monitoring Usage - Anthropic Docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://opentelemetry.io/docs/collector/&quot;&gt;OpenTelemetry Collector&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;footnotes&quot;&gt;
&lt;hr&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_1&quot;&gt;
&lt;a href=&quot;#_footnoteref_1&quot;&gt;1&lt;/a&gt;. 턴당 입력 토큰의 이차 증가에 대해서는 &lt;a href=&quot;claude-code-cost.html&quot;&gt;Claude Code 토큰 비용과 프롬프트 캐싱&lt;/a&gt;을 참고
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_2&quot;&gt;
&lt;a href=&quot;#_footnoteref_2&quot;&gt;2&lt;/a&gt;. Prompt Caching의 상세 원리는 &lt;a href=&quot;claude-code-cost.html&quot;&gt;Claude Code 토큰 비용과 프롬프트 캐싱&lt;/a&gt;을 참고
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Claude Code Hook으로 quota 전환 자동화하기</title>
      <link>https://blog.benelog.net//claude-code-quota-switch.html</link>
      <pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">claude-code-quota-switch.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#배경&quot;&gt;배경&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#전체_구성&quot;&gt;전체 구성&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#필요_환경&quot;&gt;필요 환경&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#원리_quota_소진_시_claude_code의_동작&quot;&gt;원리: Quota 소진 시 Claude Code의 동작&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#실행_흐름&quot;&gt;실행 흐름&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#구현과_테스트_프롬프트&quot;&gt;구현과 테스트 프롬프트&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#구현_프롬프트&quot;&gt;구현 프롬프트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#테스트_프롬프트&quot;&gt;테스트 프롬프트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#사용_방법&quot;&gt;사용 방법&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#1단계_quota_a_소진&quot;&gt;1단계: quota A 소진&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2단계_quota_b로_전환&quot;&gt;2단계: quota B로 전환&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3단계_자동_복귀&quot;&gt;3단계: 자동 복귀&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#생성된_코드_분석&quot;&gt;생성된 코드 분석&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#quota_소진_감지_및_초기화_시간_자동_기록&quot;&gt;Quota 소진 감지 및 초기화 시간 자동 기록&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#초기화_시간_수동_기록_fallback&quot;&gt;초기화 시간 수동 기록 (fallback)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#자동_복귀&quot;&gt;자동 복귀&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#settings_json_hook_설정&quot;&gt;settings.json hook 설정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#한계&quot;&gt;한계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#macos&quot;&gt;macOS에서의 차이점&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code를 여러 quota로 사용하는 경우가 있습니다. 예를 들면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Max plan을 2개를 구독해서 번갈아 가면서 쓰는 경우&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;같은 이메일 계정에 여러 organization이 연결되어 각각 별도의 quota가 있는 경우&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Team plan과 Enterprise plan을 동시에 구독하는 경우&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 상황에서 한쪽 quota가 소진되면 다른 쪽으로 전환해서 쓸 수 있습니다. 문제는 quota A가 소진되어 quota B로 전환한 뒤, quota A가 초기화되는 시점에 다시 돌아오는 걸 잊기 쉽다는 것입니다. Claude Code의 &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/hooks&quot;&gt;Hook&lt;/a&gt; 기능을 활용하면 이 전환을 자동화할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글은 Claude Code에게 프롬프트를 내려서 hook 스크립트를 생성하고, 생성된 코드를 분석하는 방식으로 구성됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;전체_구성&quot;&gt;전체 구성&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;필요_환경&quot;&gt;필요 환경&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Claude Code v2.1.78(2026년 3월 18일 릴리즈) 이상 (&lt;code&gt;StopFailure&lt;/code&gt; hook 이벤트 지원)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; 설치 필요&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;원리_quota_소진_시_claude_code의_동작&quot;&gt;원리: Quota 소진 시 Claude Code의 동작&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;사용자에게_보여주는_메시지&quot;&gt;사용자에게 보여주는 메시지&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code에서는 API quota를 소진 시 초기화 예정 시간을 포함한 메시지를 안내합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;5시간 한도 소진 시&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;You&apos;ve hit your limit · resets 2pm (Asia/Seoul)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주간 한도 소진 시에는 날짜도 포함됩니다:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;주간 한도 소진 시&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;You&apos;ve hit your limit · resets Feb 20, 5pm (Asia/Seoul)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메시지 형식은 Claude Code 버전에 따라 달라져 왔습니다. &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/5977&quot;&gt;과거&lt;/a&gt;에는 &lt;code&gt;Claude usage limit reached. Your limit will reset at 2pm (America/New_York)&lt;/code&gt; 형식이었고, &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/6457&quot;&gt;중간&lt;/a&gt;에 &lt;code&gt;5-hour limit reached · resets 12pm&lt;/code&gt; 을 거쳐, &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/24428&quot;&gt;현재&lt;/a&gt;의 &lt;code&gt;You&amp;#8217;ve hit your limit · resets &amp;#8230;&amp;#8203;&lt;/code&gt; 형식이 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;stopfailure_hook_이벤트&quot;&gt;StopFailure hook 이벤트&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quota 소진 시 발생하는 hook 이벤트는 &lt;code&gt;StopFailure&lt;/code&gt; 입니다 (2026년 3월 18일 릴리즈된 v2.1.78에서 도입). &lt;code&gt;Notification&lt;/code&gt; 이나 &lt;code&gt;Stop&lt;/code&gt; 은 quota 소진 시 발생하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;StopFailure&lt;/code&gt; 이벤트의 JSON 입력 (실제 기록된 데이터)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;session_id&quot;: &quot;5cbd3730-...&quot;,
  &quot;transcript_path&quot;: &quot;/home/user/.claude/projects/.../abc123.jsonl&quot;,
  &quot;cwd&quot;: &quot;/home/user/project&quot;,
  &quot;hook_event_name&quot;: &quot;StopFailure&quot;,
  &quot;error&quot;: &quot;rate_limit&quot;,
  &quot;last_assistant_message&quot;: &quot;You&apos;ve hit your limit · resets 11pm (Asia/Seoul)&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;초기화 시간 전용 필드는 없지만 (&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/36056&quot;&gt;#36056&lt;/a&gt;, &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/30784&quot;&gt;#30784&lt;/a&gt;에서 기능 요청 중), &lt;code&gt;last_assistant_message&lt;/code&gt; 필드에 초기화 시간이 포함된 메시지가 담겨 옵니다. `on-rate-limit.sh`에서 이 필드를 grep하여 초기화 시간을 추출합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;실행_흐름&quot;&gt;실행 흐름&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래는 quota 전환 자동화의 전체 흐름입니다. 스크립트 3개와 hook 2개로 구성됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;[Quota A 소진]
    │
    ▼
StopFailure hook (rate_limit) ──→ on-rate-limit.sh
    │                                  │
    │                          last_assistant_message에서 초기화 시간 파싱
    │                                  │
    │                                  ▼
    │                          quota-switch-reset-time 파일에 기록
    │
    ▼
[사용자가 /logout 후 Quota B로 로그인]
    │
    ▼
[Quota B로 작업 계속]
    │
    ▼
[초기화 시간 경과 후 새 세션 or /clear]
    │
    ▼
SessionStart hook ──→ check-quota-switch.sh
                          │
                  시간 비교: 현재 &amp;gt;= 초기화 시간?
                          │
                    Yes ──┤
                          │
                  credentials 삭제 + 세션 블로킹
                          │
                          ▼
                  [Claude Code 재시작 → Quota A로 로그인]&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;스크립트&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;역할&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;호출 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;on-rate-limit.sh&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Hook 입력의 &lt;code&gt;last_assistant_message&lt;/code&gt; 에서 초기화 시간을 파싱하여 기록&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;StopFailure&lt;/code&gt; hook (&lt;code&gt;rate_limit&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;check-quota-switch.sh&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;초기화 시간이 지났으면 credentials를 삭제하여 재로그인 유도&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;SessionStart&lt;/code&gt; hook (&lt;code&gt;startup&lt;/code&gt;, &lt;code&gt;clear&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;record-quota-reset.sh&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;초기화 시간을 수동으로 기록 (자동 파싱 실패 시 fallback)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용자가 직접 실행&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;구현과_테스트_프롬프트&quot;&gt;구현과 테스트 프롬프트&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;구현_프롬프트&quot;&gt;구현 프롬프트&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같은 프롬프트로 Claude Code에게 구현을 요청합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code를 quota A, quota B 두 개로 쓰고 있어. Quota A가 소진되면 quota B로 전환해서 쓰다가, quota A의 초기화 시간이 지난 후 새 세션이나 /clear 시에 자동으로 quota B를 logout시켜서 quota A로 복귀하고 싶어. 아래 요구사항대로 구현해줘.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;quota 소진 감지: StopFailure hook에 rate_limit matcher를 걸어서 소진 시점을 감지해줘. 스크립트 이름은 on-rate-limit.sh로 해줘.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;초기화 시간 자동 기록: on-rate-limit.sh에서 hook 입력의 last_assistant_message 필드에서 &quot;resets TIME (TIMEZONE)&quot; 패턴을 파싱하여 초기화 시간을 ~/.claude/quota-switch-reset-time에 epoch으로 저장해줘.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;초기화 시간 수동 기록(fallback): 자동 파싱이 실패할 경우를 대비해 record-quota-reset.sh도 만들어줘. &quot;4h&quot;, &quot;30m&quot; 같은 상대 시간과 절대 시간을 지원해야해.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;자동 복귀: SessionStart hook에서 check-quota-switch.sh를 실행해서, 현재 시간이 초기화 시간을 지났으면 credentials를 삭제하여 재로그인을 유도해줘. startup과 clear 두 matcher 모두 등록해줘.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code는 이 요청을 받아 세 개의 스크립트 파일을 생성하고, `~/.claude/settings.json`에 hook 설정까지 직접 등록했습니다. 사용자가 `settings.json`을 수동으로 편집할 필요 없이, 프롬프트 하나로 스크립트 작성과 hook 등록이 모두 완료됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;테스트_프롬프트&quot;&gt;테스트 프롬프트&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실제 quota를 소진하지 않고도 hook과 스크립트가 정상 동작하는지 검증할 수 있습니다. 테스트 역시 Claude Code에게 요청하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;~/.claude/settings.json에 등록된 on-rate-limit.sh와 check-quota-switch.sh를 실제 quota 소진 없이 테스트하고 싶어. on-rate-limit.sh는 hook JSON 입력을 직접 구성해서 stdin으로 넘기는 방식으로 테스트해줘. &quot;resets 2pm (Asia/Seoul)&quot; 같은 5시간 한도 메시지와 &quot;resets Feb 20, 5pm (Asia/Seoul)&quot; 같은 주간 한도 메시지 두 가지를 모두 테스트해줘. 각 테스트 후 hook-events.log와 quota-switch-reset-time 파일을 확인해줘. check-quota-switch.sh는 과거 시간의 epoch을 기록한 뒤 실행해서 credentials 삭제 동작을 검증해줘. 단, 실제 credentials가 삭제되므로 테스트 전에 백업하고 테스트 후 복원해줘.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code는 이 요청에 따라 hook JSON을 직접 구성하여 `on-rate-limit.sh`에 전달하고, 결과를 확인하는 과정을 수행합니다. 예를 들어 5시간 한도 메시지 테스트는 다음과 같이 실행됩니다:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;echo &apos;{
  &quot;hook_event_name&quot;: &quot;StopFailure&quot;,
  &quot;error&quot;: &quot;rate_limit&quot;,
  &quot;last_assistant_message&quot;: &quot;You&apos;\&apos;&apos;ve hit your limit · resets 2pm (Asia/Seoul)&quot;
}&apos; | ~/.claude/scripts/on-rate-limit.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트가 성공하면 &lt;code&gt;~/.claude/hook-events.log&lt;/code&gt; 에 &quot;Auto-recorded quota reset time&quot;이 기록되고, &lt;code&gt;~/.claude/quota-switch-reset-time&lt;/code&gt; 파일에 epoch 값이 저장됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;사용_방법&quot;&gt;사용 방법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1단계_quota_a_소진&quot;&gt;1단계: quota A 소진&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quota A가 소진되면 &lt;code&gt;StopFailure&lt;/code&gt; hook이 자동으로 &lt;code&gt;on-rate-limit.sh&lt;/code&gt; 를 실행합니다. Hook 입력의 &lt;code&gt;last_assistant_message&lt;/code&gt; 에서 초기화 시간을 파싱하여 `~/.claude/quota-switch-reset-time`에 기록합니다. `~/.claude/hook-events.log`에서 &quot;Auto-recorded quota reset time&quot;이 기록되었는지 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자동 파싱에 실패한 경우, 로그에 실패 사유가 기록됩니다. 이때는 수동으로 쿼터가 초기화될 시간을 기록합니다:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;record-quota-reset.sh 수동 실행 예시&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;# 절대 시간
~/.claude/scripts/record-quota-reset.sh &quot;2026-03-29 09:00&quot;

# 상대 시간
~/.claude/scripts/record-quota-reset.sh 4h
~/.claude/scripts/record-quota-reset.sh 30m&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2단계_quota_b로_전환&quot;&gt;2단계: quota B로 전환&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code에서 &lt;code&gt;/logout&lt;/code&gt; 을 실행한 후 quota B 계정으로 재로그인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;3단계_자동_복귀&quot;&gt;3단계: 자동 복귀&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기록된 초기화 시간이 지난 후에 Claude Code를 새로 시작하거나 &lt;code&gt;/clear&lt;/code&gt; 를 실행하면 &lt;code&gt;SessionStart&lt;/code&gt; hook이 동작합니다. Quota B의 credentials가 자동으로 제거되고, Claude Code를 재시작하면 quota A로 로그인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;생성된_코드_분석&quot;&gt;생성된 코드 분석&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 코드는 Linux 환경에서 생성된 것입니다. macOS에서의 차이점은 &lt;a href=&quot;#macos&quot;&gt;macOS에서의 차이점&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;quota_소진_감지_및_초기화_시간_자동_기록&quot;&gt;Quota 소진 감지 및 초기화 시간 자동 기록&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;~/.claude/scripts/on-rate-limit.sh&lt;/code&gt; 는 &lt;code&gt;StopFailure&lt;/code&gt; hook에서 실행되는 스크립트입니다. Quota 소진 시 hook 입력의 &lt;code&gt;last_assistant_message&lt;/code&gt; 필드에서 초기화 시간을 파싱하여 기록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;on-rate-limit.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/bin/bash
# StopFailure hook (matcher: rate_limit)
# 1. Log the event for diagnostics
# 2. Extract reset time from last_assistant_message (hook JSON input)

LOG_FILE=&quot;$HOME/.claude/hook-events.log&quot;
INPUT=$(cat)

# Step 1: Log the event
echo &quot;=== $(date &apos;+%Y-%m-%d %H:%M:%S&apos;) ===&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;
echo &quot;$INPUT&quot; | jq . &amp;gt;&amp;gt; &quot;$LOG_FILE&quot; 2&amp;gt;/dev/null || echo &quot;$INPUT&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;
echo &quot;&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;

# Step 2: Extract reset time from last_assistant_message
LAST_MSG=$(echo &quot;$INPUT&quot; | jq -r &apos;.last_assistant_message // empty&apos;)
if [ -z &quot;$LAST_MSG&quot; ]; then
    echo &quot;No last_assistant_message in hook input&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;
    exit 0
fi

RESET_LINE=$(echo &quot;$LAST_MSG&quot; | grep -oP \
    &apos;resets?\s+(at\s+)?(\w+\s+\d+,?\s+)?\d{1,2}(:\d{2})?\s*[ap]m(\s*\([^)]+\))?&apos;)

if [ -z &quot;$RESET_LINE&quot; ]; then
    echo &quot;No reset time found in message: $LAST_MSG&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;
    exit 0
fi

echo &quot;Found reset info: $RESET_LINE&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;

# Step 3: Parse the reset time
TIME_PART=$(echo &quot;$RESET_LINE&quot; | grep -oP &apos;\d{1,2}(:\d{2})?\s*[ap]m&apos;)
DATE_PART=$(echo &quot;$RESET_LINE&quot; | grep -oP &apos;(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}&apos; | head -1)
TZ_PART=$(echo &quot;$RESET_LINE&quot; | grep -oP &apos;\(([^)]+)\)&apos; | tr -d &apos;()&apos;)

# Convert 12-hour to 24-hour
HOUR=$(echo &quot;$TIME_PART&quot; | grep -oP &apos;^\d{1,2}&apos;)
MINUTE=$(echo &quot;$TIME_PART&quot; | grep -oP &apos;:\K\d{2}&apos; || echo &quot;00&quot;)
AMPM=$(echo &quot;$TIME_PART&quot; | grep -oP &apos;[ap]m&apos;)

if [ &quot;$AMPM&quot; = &quot;pm&quot; ] &amp;amp;&amp;amp; [ &quot;$HOUR&quot; -ne 12 ]; then
    HOUR=$((HOUR + 12))
elif [ &quot;$AMPM&quot; = &quot;am&quot; ] &amp;amp;&amp;amp; [ &quot;$HOUR&quot; -eq 12 ]; then
    HOUR=0
fi
TIME_24=$(printf &quot;%02d:%s&quot; &quot;$HOUR&quot; &quot;$MINUTE&quot;)

# Build datetime string
TODAY=$(date &quot;+%Y-%m-%d&quot;)
if [ -n &quot;$DATE_PART&quot; ]; then
    YEAR=$(date &quot;+%Y&quot;)
    DATETIME=&quot;$DATE_PART $YEAR $TIME_24&quot;
else
    DATETIME=&quot;$TODAY $TIME_24&quot;
fi

# Convert to epoch
if [ -n &quot;$TZ_PART&quot; ]; then
    RESET_EPOCH=$(TZ=&quot;$TZ_PART&quot; date -d &quot;$DATETIME&quot; +%s 2&amp;gt;/dev/null)
else
    RESET_EPOCH=$(date -d &quot;$DATETIME&quot; +%s 2&amp;gt;/dev/null)
fi

if [ -z &quot;$RESET_EPOCH&quot; ]; then
    echo &quot;Failed to parse reset time: $DATETIME (TZ=$TZ_PART)&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;
    exit 0
fi

# If parsed time is in the past, assume next day (5-hour limit case)
NOW_EPOCH=$(date +%s)
if [ &quot;$RESET_EPOCH&quot; -le &quot;$NOW_EPOCH&quot; ] &amp;amp;&amp;amp; [ -z &quot;$DATE_PART&quot; ]; then
    RESET_EPOCH=$((RESET_EPOCH + 86400))
fi

# Step 4: Record the reset time
RESET_FILE=&quot;$HOME/.claude/quota-switch-reset-time&quot;
echo &quot;$RESET_EPOCH&quot; &amp;gt; &quot;$RESET_FILE&quot;
RESET_DISPLAY=$(date -d &quot;@$RESET_EPOCH&quot; &quot;+%Y-%m-%d %H:%M:%S&quot;)
echo &quot;Auto-recorded quota reset time: $RESET_DISPLAY&quot; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;

exit 0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;last_assistant_message&lt;/code&gt; 필드에서 &lt;code&gt;resets TIME (TIMEZONE)&lt;/code&gt; 패턴을 grep하여 초기화 시간을 추출하는 원리는 &lt;a href=&quot;#stopfailure_hook_이벤트&quot;&gt;StopFailure hook 이벤트&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;초기화_시간_수동_기록_fallback&quot;&gt;초기화 시간 수동 기록 (fallback)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;~/.claude/scripts/record-quota-reset.sh&lt;/code&gt; 는 자동 파싱이 실패했을 때 초기화 시간을 수동으로 기록하는 스크립트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;record-quota-reset.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/bin/bash
# Record when quota A resets so the hook can switch back from quota B
# Usage: record-quota-reset.sh &quot;2026-03-29 09:00&quot;
#        record-quota-reset.sh 4h    (4 hours from now)
#        record-quota-reset.sh 30m   (30 minutes from now)

RESET_FILE=&quot;$HOME/.claude/quota-switch-reset-time&quot;

if [ -z &quot;$1&quot; ]; then
    echo &quot;Usage: $0 &amp;lt;datetime or duration&amp;gt;&quot;
    echo &quot;  Examples:&quot;
    echo &quot;    $0 &apos;2026-03-29 09:00&apos;   # absolute time&quot;
    echo &quot;    $0 4h                    # 4 hours from now&quot;
    echo &quot;    $0 30m                   # 30 minutes from now&quot;
    exit 1
fi

INPUT=&quot;$1&quot;

# Parse duration format (e.g., 4h, 30m)
if [[ &quot;$INPUT&quot; =~ ^([0-9]+)h$ ]]; then
    HOURS=&quot;${BASH_REMATCH[1]}&quot;
    RESET_EPOCH=$(date -d &quot;+${HOURS} hours&quot; +%s)
    RESET_DISPLAY=$(date -d &quot;+${HOURS} hours&quot; &quot;+%Y-%m-%d %H:%M:%S&quot;)
elif [[ &quot;$INPUT&quot; =~ ^([0-9]+)m$ ]]; then
    MINS=&quot;${BASH_REMATCH[1]}&quot;
    RESET_EPOCH=$(date -d &quot;+${MINS} minutes&quot; +%s)
    RESET_DISPLAY=$(date -d &quot;+${MINS} minutes&quot; &quot;+%Y-%m-%d %H:%M:%S&quot;)
else
    # Absolute datetime
    RESET_EPOCH=$(date -d &quot;$INPUT&quot; +%s 2&amp;gt;/dev/null)
    if [ $? -ne 0 ]; then
        echo &quot;Error: Cannot parse datetime &apos;$INPUT&apos;&quot;
        exit 1
    fi
    RESET_DISPLAY=$(date -d &quot;$INPUT&quot; &quot;+%Y-%m-%d %H:%M:%S&quot;)
fi

echo &quot;$RESET_EPOCH&quot; &amp;gt; &quot;$RESET_FILE&quot;
echo &quot;Quota A reset time recorded: $RESET_DISPLAY&quot;
echo &quot;Saved to: $RESET_FILE&quot;
echo &quot;&quot;
echo &quot;Now run /logout in Claude Code and log in with quota B.&quot;
echo &quot;When the reset time arrives, the next session start or /clear will auto-logout quota B.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;절대 시간(&lt;code&gt;&quot;2026-03-29 09:00&quot;&lt;/code&gt;)과 상대 시간(&lt;code&gt;4h&lt;/code&gt;, &lt;code&gt;30m&lt;/code&gt;) 두 가지 형식을 지원합니다. 내부적으로는 Unix epoch 타임스탬프로 변환하여 &lt;code&gt;~/.claude/quota-switch-reset-time&lt;/code&gt; 파일에 저장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;자동_복귀&quot;&gt;자동 복귀&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;~/.claude/scripts/check-quota-switch.sh&lt;/code&gt; 는 SessionStart hook에서 실행되는 스크립트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;check-quota-switch.sh&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/bin/bash
# SessionStart hook: check if quota A has reset
# If so, remove credentials to force re-login with quota A

RESET_FILE=&quot;$HOME/.claude/quota-switch-reset-time&quot;
CRED_FILE=&quot;$HOME/.claude/.credentials.json&quot;

# No reset time recorded, nothing to do
if [ ! -f &quot;$RESET_FILE&quot; ]; then
    exit 0
fi

RESET_EPOCH=$(cat &quot;$RESET_FILE&quot;)
NOW_EPOCH=$(date +%s)

if [ &quot;$NOW_EPOCH&quot; -ge &quot;$RESET_EPOCH&quot; ]; then
    # Time to switch back to quota A
    RESET_DISPLAY=$(date -d &quot;@$RESET_EPOCH&quot; &quot;+%Y-%m-%d %H:%M:%S&quot;)

    # Backup quota B credentials and remove current credentials
    if [ -f &quot;$CRED_FILE&quot; ]; then
        cp &quot;$CRED_FILE&quot; &quot;$HOME/.claude/.credentials.quota-b.bak&quot;
        rm -f &quot;$CRED_FILE&quot;
    fi

    # Remove reset time file
    rm -f &quot;$RESET_FILE&quot;

    # Exit with code 2 to block session and show message
    echo &quot;Quota A has reset (reset time: $RESET_DISPLAY).&quot; &amp;gt;&amp;amp;2
    echo &quot;Quota B credentials have been removed.&quot; &amp;gt;&amp;amp;2
    echo &quot;Please restart Claude Code to log in with quota A.&quot; &amp;gt;&amp;amp;2
    exit 2
fi

# Not yet time to switch
exit 0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 스크립트의 핵심 동작은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;기록된 초기화 시간 파일이 없으면 `exit 0`으로 아무 동작 없이 종료합니다. 평소에는 hook이 세션 시작을 방해하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;현재 시간이 초기화 시간 이상이면 전환을 실행합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;~/.claude/.credentials.json&lt;/code&gt; 을 &lt;code&gt;~/.claude/.credentials.quota-b.bak&lt;/code&gt; 으로 백업한 후 삭제합니다. Credentials 파일이 없으면 Claude Code가 다음 시작 시 로그인을 요청합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`exit 2`로 세션을 블로킹하고 stderr로 안내 메시지를 출력합니다. Claude Code hook에서 exit code 2는 세션 진행을 차단하고 stderr 메시지를 사용자에게 보여주는 약속된 규칙입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;settings_json_hook_설정&quot;&gt;settings.json hook 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;~/.claude/settings.json&lt;/code&gt; 에 &lt;code&gt;StopFailure`와 `SessionStart&lt;/code&gt; hook을 등록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;hooks&quot;: {
    &quot;StopFailure&quot;: [
      {
        &quot;matcher&quot;: &quot;rate_limit&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;/home/user/.claude/scripts/on-rate-limit.sh&quot;
          }
        ]
      }
    ],
    &quot;SessionStart&quot;: [
      {
        &quot;matcher&quot;: &quot;startup&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;/home/user/.claude/scripts/check-quota-switch.sh&quot;
          }
        ]
      },
      {
        &quot;matcher&quot;: &quot;clear&quot;,
        &quot;hooks&quot;: [
          {
            &quot;type&quot;: &quot;command&quot;,
            &quot;command&quot;: &quot;/home/user/.claude/scripts/check-quota-switch.sh&quot;
          }
        ]
      }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;&quot;matcher&quot;: &quot;rate_limit&quot;&lt;/code&gt; 으로 quota 소진 이벤트만 필터링합니다. &lt;code&gt;SessionStart&lt;/code&gt; 이벤트는 &lt;code&gt;startup&lt;/code&gt; (새 세션 시작)과 &lt;code&gt;clear&lt;/code&gt; (&lt;code&gt;/clear&lt;/code&gt; 실행) 두 경우 모두 같은 스크립트를 실행하도록 설정했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;한계&quot;&gt;한계&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;credentials 저장소 의존&lt;/strong&gt;: Linux에서는 &lt;code&gt;~/.claude/.credentials.json&lt;/code&gt; 파일을, macOS에서는 Keychain을 직접 조작하는 방식이므로, Claude Code의 내부 인증 구조가 변경되면 동작하지 않을 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;완전 자동 로그인은 불가&lt;/strong&gt;: Credentials 제거 후 재시작 시 로그인 화면이 나타나지만, quota A 계정을 자동으로 선택해주지는 않습니다. 사용자가 직접 quota A 계정으로 로그인해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;세션 중간 전환은 지원하지 않음&lt;/strong&gt;: &lt;code&gt;SessionStart&lt;/code&gt; hook은 새 세션 시작이나 &lt;code&gt;/clear&lt;/code&gt; 시에만 동작합니다. 세션 중간에 초기화 시간이 지나더라도 현재 세션에서는 전환이 일어나지 않습니다. 매 프롬프트마다 체크하는 방식(&lt;code&gt;UserPromptSubmit&lt;/code&gt; hook)도 가능하지만, 세션 중간에 credentials가 제거되면 진행 중인 작업과 컨텍스트가 유실되므로 권장하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;메시지 형식 의존&lt;/strong&gt;: &lt;code&gt;on-rate-limit.sh`의 파싱은 `last_assistant_message&lt;/code&gt; 필드의 &lt;code&gt;resets TIME (TIMEZONE)&lt;/code&gt; 패턴에 의존합니다. Claude Code 버전에 따라 메시지 형식이 달라져 왔으므로, 향후 형식이 변경되면 파싱이 실패할 수 있습니다. 실패 시 &lt;code&gt;~/.claude/hook-events.log&lt;/code&gt; 에 원인이 기록되며, `record-quota-reset.sh`로 수동 기록할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;macos&quot;&gt;macOS에서의 차이점&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code는 실행 환경의 OS를 인식합니다. macOS에서 같은 요청을 하면 &lt;code&gt;date&lt;/code&gt; 명령어 부분이 BSD 문법으로 생성됩니다. 주요 차이는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;용도&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Linux (GNU date)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;macOS (BSD date)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;상대 시간 (4시간 후)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;date -d &quot;+4 hours&quot; +%s&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;date -v+4H +%s&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;절대 시간 파싱&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;date -d &quot;2026-03-29 09:00&quot; +%s&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;date -j -f &quot;%Y-%m-%d %H:%M&quot; &quot;2026-03-29 09:00&quot; +%s&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;epoch → 표시 형식&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;date -d &quot;@$EPOCH&quot; &quot;+%Y-%m-%d %H:%M:%S&quot;&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;date -r $EPOCH &quot;+%Y-%m-%d %H:%M:%S&quot;&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 외에도 macOS에서는 다음 차이가 있습니다:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;grep -oP&lt;/code&gt; (PCRE)가 macOS 기본 grep에서 지원되지 않습니다. &lt;code&gt;brew install grep&lt;/code&gt; 후 `ggrep`을 사용하거나, 정규식을 POSIX 호환으로 변경해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;macOS에서 Claude Code의 credentials는 &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/security#credential-storage&quot;&gt;macOS Keychain에 저장&lt;/a&gt;됩니다. &lt;code&gt;~/.claude/.credentials.json&lt;/code&gt; 파일이 존재하지 않으므로, &lt;code&gt;check-quota-switch.sh&lt;/code&gt;에서 파일을 삭제하는 방식이 동작하지 않습니다. macOS에서 프롬프트를 내리면 &lt;code&gt;security delete-generic-password&lt;/code&gt; 명령으로 Keychain에서 credentials를 삭제하는 스크립트가 생성됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Go 코드에 품질 도구 적용하기</title>
      <link>https://blog.benelog.net//go-quality-tools.html</link>
      <pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">go-quality-tools.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#gofmt_코드_포맷팅&quot;&gt;1. gofmt: 코드 포맷팅&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#goimports_import_정리&quot;&gt;2. goimports: import 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#go_vet_정적_분석&quot;&gt;3. go vet: 정적 분석&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#golangci_lint_통합_린터&quot;&gt;4. golangci-lint: 통합 린터&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#설정_파일&quot;&gt;4.1. 설정 파일&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#기본_활성화_린터_default_standard&quot;&gt;4.2. 기본 활성화 린터 (&lt;code&gt;default: standard&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#makefile로_통합&quot;&gt;5. Makefile로 통합&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;6. 참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Go는 언어 차원에서 코드 포맷팅 도구(&lt;code&gt;gofmt&lt;/code&gt;)를 제공하고, 표준 도구 체인에 정적 분석(&lt;code&gt;go vet&lt;/code&gt;)이 포함되어 있습니다.
이런 도구들을 프로젝트 초기에 설정해두면 코드 리뷰에서 스타일 논쟁을 줄이고, 흔한 실수를 빌드 전에 잡을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글에서는 Go로 개발하는 프로젝트에서 기본적으로 갖추면 좋은 도구들과 설정 방법을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gofmt_코드_포맷팅&quot;&gt;1. gofmt: 코드 포맷팅&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Go 커뮤니티에서는 &lt;code&gt;gofmt&lt;/code&gt; 이 표준 포매터입니다.
탭/스페이스, 중괄호 위치 같은 스타일 논쟁 없이 모든 Go 코드가 동일한 포맷을 따르게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gofmt -w .&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;-w&lt;/code&gt; 플래그는 파일을 직접 수정합니다. &lt;code&gt;-l&lt;/code&gt; 플래그를 쓰면 포맷이 맞지 않는 파일 목록만 출력합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;gofmt -l .&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;포맷이 맞지 않는 파일이 있으면 파일 경로가 출력되고, 모두 맞으면 아무것도 출력되지 않습니다.
CI에서는 이 출력을 검사해서 포맷팅되지 않은 코드가 머지되는 것을 막을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;goimports_import_정리&quot;&gt;2. goimports: import 정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://pkg.go.dev/golang.org/x/tools/cmd/goimports&quot;&gt;goimports&lt;/a&gt;는 `gofmt`의 기능에 더해 import 문을 자동으로 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;사용하지 않는 import를 제거&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;필요한 import를 자동 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;import를 표준 라이브러리 / 그 외 패키지 2그룹으로 정렬 (&lt;code&gt;-local&lt;/code&gt; 플래그를 쓰면 로컬 패키지를 별도 그룹으로 분리 가능)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;go install golang.org/x/tools/cmd/goimports@latest
goimports -w .&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gofmt&lt;/code&gt; 대신 `goimports`만 실행해도 포맷팅까지 함께 처리됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;go_vet_정적_분석&quot;&gt;3. go vet: 정적 분석&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`go vet`은 Go 표준 도구 체인에 포함된 정적 분석 도구입니다.
컴파일러가 잡지 못하는 의심스러운 코드를 검출합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;go vet ./...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대표적으로 잡아주는 문제들은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;fmt.Printf`의 포맷 문자열과 인자 불일치 (`printf&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;도달할 수 없는 코드 (&lt;code&gt;unreachable&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sync.Mutex&lt;/code&gt; 등 잠금을 값으로 복사하는 코드 (&lt;code&gt;copylocks&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;context.WithCancel`의 cancel 함수를 호출하지 않는 경로 (`lostcancel&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;별도 설치 없이 바로 사용할 수 있으므로, 모든 Go 프로젝트에서 기본으로 실행하는 것이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;golangci_lint_통합_린터&quot;&gt;4. golangci-lint: 통합 린터&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://golangci-lint.run/&quot;&gt;golangci-lint&lt;/a&gt;는 여러 린터를 하나의 도구로 통합해서 실행해주는 린터 러너입니다.
개별 린터를 따로 설치하고 실행할 필요 없이, 설정 파일 하나로 원하는 린터를 선택해서 쓸 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;## 바이너리 설치 (go install은 공식적으로 권장되지 않음)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin
golangci-lint run ./...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;설정_파일&quot;&gt;4.1. 설정 파일&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;프로젝트 루트에 &lt;code&gt;.golangci.yml&lt;/code&gt; 파일을 만들어 설정합니다.
아래는 golangci-lint v2 기준 설정 예시입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.golangci.yml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;version: &quot;2&quot;

linters:
  default: standard
  enable:
    - misspell

formatters:
  enable:
    - gofmt
    - goimports&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;v2에서는 `version: &quot;2&quot;`를 반드시 명시해야 합니다.
`default: standard`로 설정하면 errcheck, govet, ineffassign, staticcheck, unused 5개 린터가 기본으로 활성화됩니다.
추가로 필요한 린터만 `enable`에 넣으면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;v2의 또 다른 변경점은 gofmt, goimports 같은 포매터가 &lt;code&gt;linters`가 아닌 `formatters&lt;/code&gt; 섹션으로 분리된 것입니다.
기존 v1 설정 파일이 있다면 &lt;code&gt;golangci-lint migrate&lt;/code&gt; 명령으로 자동 변환할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;기본_활성화_린터_default_standard&quot;&gt;4.2. 기본 활성화 린터 (&lt;code&gt;default: standard&lt;/code&gt;)&lt;/h3&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;린터&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;errcheck&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;에러 반환값을 무시하는 코드 검출&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;govet&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;`go vet`과 동일한 정적 분석&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ineffassign&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;이후에 사용되지 않는 변수 할당 검출&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;staticcheck&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Go 코드의 버그, 성능 문제, 스타일 문제를 종합적으로 검사 (gosimple, stylecheck 포함)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;unused&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;사용되지 않는 변수, 함수, 타입 검출&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 중 `errcheck`은 특히 중요합니다. Go에서 에러를 반환값으로 처리하는 관례상, 에러를 무시하는 코드는 런타임에 예상치 못한 동작을 일으킬 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;makefile로_통합&quot;&gt;5. Makefile로 통합&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 도구들을 개별로 실행하는 것은 번거로우므로, Makefile에 통합하면 편리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Makefile&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;makefile&quot;&gt;.PHONY: fmt lint test check ci

fmt:
	goimports -w .

lint:
	golangci-lint run ./...

test:
	go test ./...

check: fmt lint test

ci: lint test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;check`와 `ci&lt;/code&gt; 두 가지 타겟을 나눈 이유가 있습니다.
&lt;code&gt;fmt&lt;/code&gt; 타겟은 파일을 직접 수정하므로 로컬에서 커밋 전에 실행하기에 편리하지만, CI 환경에서는 파일을 수정하면 안 됩니다.
&lt;code&gt;ci&lt;/code&gt; 타겟은 `golangci-lint`의 gofmt, goimports 린터가 포맷 위반을 검출만 하고 파일을 수정하지 않으므로, CI에서 안전하게 쓸 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;로컬: &lt;code&gt;make check&lt;/code&gt; (포맷 자동 수정 + 린트 + 테스트)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CI: &lt;code&gt;make ci&lt;/code&gt; (린트 + 테스트, 파일 수정 없음)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`go vet`을 별도 타겟으로 두지 않은 이유는, golangci-lint에 govet 린터가 포함되어 있어서 `make lint`에서 이미 실행되기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코드 수정 후 커밋하기 전에 `make check`를 실행하는 습관을 들이면, CI에서 실패하는 상황을 줄일 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;6. 참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/blog/gofmt&quot;&gt;go fmt your code - The Go Blog&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://pkg.go.dev/golang.org/x/tools/cmd/goimports&quot;&gt;goimports - pkg.go.dev&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://pkg.go.dev/cmd/vet&quot;&gt;go vet - pkg.go.dev&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://golangci-lint.run/&quot;&gt;golangci-lint 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이 글은 개인 라이선스로 구매한 Claude Code의 도움을 받아 작성되었습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Claude Code의 &apos;/model opusplan&amp;#8217;과 showClearContextOnPlanAccept 옵션</title>
      <link>https://blog.benelog.net//claude-code-opusplan.html</link>
      <pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">claude-code-opusplan.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#model_opusplan&quot;&gt;/model opusplan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#showclearcontextonplanaccept_설정&quot;&gt;showClearContextOnPlanAccept 설정&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#showclearcontextonplanaccept은_기본값_변경_이력&quot;&gt;showClearContextOnPlanAccept은 기본값 변경 이력&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#plan_실행_전_컨텍스트_클리어의_트레이드오프&quot;&gt;Plan 실행 전 컨텍스트 클리어의 트레이드오프&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;model_opusplan&quot;&gt;/model opusplan&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;/model opusplan&lt;/code&gt; 을 선택하면 Opus로 plan을 세우고 Sonnet으로 실행하는 전략을 쓸 수 있습니다. 복잡한 작업에서 Opus의 추론 능력으로 계획을 세우고, 실행은 비용이 낮은 Sonnet으로 처리하는 방식입니다.
그런데 같은 세션 안에서 모델이 전환되면, 그때까지 쌓인 히스토리 전체가 캐시 미스로 재과금됩니다. 턴이 많이 진행된 후에 전환할수록 삼각수 효과로 누적된 토큰이 크기 때문에 비용 타격이 큽니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock tip&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Tip&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
토큰 비용의 삼각수 효과와 Prompt Caching에 대한 자세한 설명은 &lt;a href=&quot;claude-code-cost.html&quot;&gt;Claude Code 토큰 비용과 프롬프트 캐싱&lt;/a&gt;을 참고하세요.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;showclearcontextonplanaccept_설정&quot;&gt;showClearContextOnPlanAccept 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세션 중간에 모델이 전환될 때 발생하는 부정적인 효과를 줄이는데 도움이 될 수도 있는 옵션이 있습니다. &lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/settings&quot;&gt;Claude Code 설정&lt;/a&gt;의 &lt;code&gt;showClearContextOnPlanAccept&lt;/code&gt; 옵션이 &lt;code&gt;true&lt;/code&gt; 이면, plan 승인 시 컨텍스트를 클리어할지 선택하는 옵션이 나타납니다.
&apos;Yes, clear context(??% used) and by pass permissions&apos; 를 선택하면, 컨텍스트를 클리어하면 planning 단계에서 쌓인 히스토리가 제거된 상태에서 실행이 시작되므로, 모델 전환으로 인한 캐시 무효화 비용을 피할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 선택지가 나오도록 활성화하려면 &lt;code&gt;~/.claude/settings.json&lt;/code&gt; 에 다음과 같이 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &quot;preferences&quot;: {
    &quot;showClearContextOnPlanAccept&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;showclearcontextonplanaccept은_기본값_변경_이력&quot;&gt;showClearContextOnPlanAccept은 기본값 변경 이력&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md&quot;&gt;Claude Code의 CHANGELOG&lt;/a&gt;에 따르면 showClearContextOnPlanAccept은 아래와 같은 이력을 가지고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;v2.1.0(2026-01-07) : &quot;Clear context&quot; 옵션이 plan 승인 시 기본 선택으로 처음 도입&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.2(2026-01-09) : Shift+Tab 단축키로 이 옵션을 건너뛸 수 있는 기능도 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;v2.1.81(2026-03-20) : 기본값이 &lt;code&gt;false&lt;/code&gt; 로 변경&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 2026년 3월 말 현 시점에서는 showClearContextOnPlanAccept 속성값을 직접 추가하지 않으면 &quot;clear context&quot; 옵션이 나타나지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;plan_실행_전_컨텍스트_클리어의_트레이드오프&quot;&gt;Plan 실행 전 컨텍스트 클리어의 트레이드오프&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 옵션은 opusplan이 아닌 경우에도 유용할 때도 있습니다. 같은 모델로 plan mode를 사용하더라도, plan 단계에서 많은 턴이 쌓였다면 컨텍스트를 클리어하고 실행을 시작하면 이후 턴의 입력 토큰 크기를 줄일 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 컨텍스트 클리어에는 트레이드오프가 있습니다. Plan 파일 자체는 디스크(&lt;code&gt;~/.claude/plans/&lt;/code&gt;)에 저장되어 보존되지만, planning 중에 읽은 파일 내용이나 탐색 결과는 모두 사라집니다. 따라서 실행 단계에서 필요한 파일을 plan을 가이드 삼아 다시 읽어야 하며, 이 과정에서 추가 토큰이 소비됩니다. planning 중의 논의 내용 중 plan 텍스트에 반영되지 않은 부분도 유실됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Plan 실행 전에 컨텍스트 클리어에 대해서 아래와 같은 부정적인 사용자 피드백이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/18523&quot;&gt;#18523&lt;/a&gt; Plan 승인 대화상자의 기본 옵션을 설정할 수 있게 해달라는 요청. 컨텍스트를 유지해야 복잡한 문제에서 더 나은 결정을 내릴 수 있고, auto-compact가 이미 컨텍스트를 관리하므로 강제 클리어가 불필요하다는 의견.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/18878&quot;&gt;#18878&lt;/a&gt; &quot;Clear context&quot; 기본 옵션을 비활성화하거나 순서를 변경할 수 있게 해달라는 요청. plan에 포함되지 않은 대화 맥락이 유실되는 문제를 지적하며, 파괴적인 동작이 기본값이 되어서는 안 된다는 의견.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/anthropics/claude-code/issues/25734&quot;&gt;#25734&lt;/a&gt; &quot;Clear context and implement&quot;가 기본 옵션인 것은 파괴적이고 되돌릴 수 없다는 문제 제기. 터미널 스크롤백까지 지워져 복구가 불가능하며, 가장 파괴적인 옵션이 기본값이 되어서는 안 된다는 의견.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Claude Code 토큰 비용과 프롬프트 캐싱</title>
      <link>https://blog.benelog.net//claude-code-cost.html</link>
      <pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">claude-code-cost.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#과금의_기본_단위_토큰&quot;&gt;과금의 기본 단위: 토큰&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#모델별_가격표&quot;&gt;모델별 가격표&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#prompt_caching의_효과&quot;&gt;Prompt Caching의 효과&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#직접_시뮬레이션해보기&quot;&gt;직접 시뮬레이션해보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude Code는 Anthropic의 API를 호출하는 CLI 도구입니다.
Enterprise Plan에서는 API 사용량에 따라 과금되므로, 과금 구조를 이해하면 같은 작업을 더 적은 비용으로 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;과금의_기본_단위_토큰&quot;&gt;과금의 기본 단위: 토큰&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude API는 토큰 단위로 과금됩니다. 토큰 종류별로 가격이 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;토큰 종류&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Input 토큰&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모델에게 보내는 모든 텍스트 (시스템 프롬프트, 대화 히스토리, 도구 정의 등)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Output 토큰&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;모델이 생성한 응답 텍스트&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Thinking 토큰&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Extended thinking 모드에서 모델이 내부적으로 추론하는 데 쓰는 토큰. Output과 같은 가격으로 과금&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;모델별_가격표&quot;&gt;모델별 가격표&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 8월 기준으로 백만(1M) 토큰당 가격은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;모델&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Input&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Output&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Cache Write&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Cache Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Fable 5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$10&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$50&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$12.50&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$1.00&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Opus 5/4.8&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$25&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$6.25&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.50&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Sonnet 5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$2&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$10&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$2.50&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.20&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Sonnet 4.6&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$3&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$15&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$3.75&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.30&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Haiku 4.5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$1&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$1.25&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;$0.10&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude 5 패밀리가 나오면서 가격 구도가 달라졌습니다.
최상위 모델인 Fable 5는 Sonnet 5의 5배, Opus 5는 2.5배입니다. Haiku 4.5는 Sonnet 5의 절반 수준입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sonnet 5는 2026년 6월 출시 당시 프로모션 가격(Input $2, Output $10)으로 시작했고, 8월 31일 이후 $3/$15로 인상될 예정이었습니다.
그런데 2026년 8월 10일에 이 프로모션 가격이 정식 가격으로 확정되었습니다. 이전 세대인 Sonnet 4.6은 기존 $3/$15를 유지합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;prompt_caching의_효과&quot;&gt;Prompt Caching의 효과&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Claude API는 stateless입니다. 매 턴마다 시스템 프롬프트, 도구 정의, 이전 대화 히스토리를 모두 다시 보내야 합니다.
여기서 &apos;턴&amp;#8217;은 사용자가 한 번 입력하고 Claude가 한 번 답변하는 단위가 아닙니다. Claude Code는 agentic하게 동작하기 때문에, 사용자가 한 번 입력하더라도 Claude가 파일 읽기, 편집, 명령 실행 등 도구를 호출할 때마다 API 호출이 발생합니다. 즉 &lt;strong&gt;한 번의 사용자 입력이 여러 턴&lt;/strong&gt;에 해당할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;턴이 진행될수록 입력 토큰이 누적되어 증가합니다. 공식으로 표현하면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;턴 n의 입력 토큰:
  input(n) = B + n·T + (O + Th)·(n − 1)

  B  = 시스템 프롬프트 + 도구 정의 (매 턴 고정)
  T  = 턴당 새 사용자 입력 토큰
  O  = 턴당 출력 토큰 (히스토리 누적)
  Th = 턴당 thinking 토큰 (히스토리 누적)

N턴까지의 총 입력 토큰:
  총 입력 = N·B + T·N(N+1)/2 + (O+Th)·N(N−1)/2&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;N(N+1)/2&lt;/code&gt; 는 삼각수 공식입니다. 즉 총 입력 토큰은 턴 수에 대해 &lt;strong&gt;이차(quadratic)로 증가&lt;/strong&gt;합니다.
10턴 대화의 뒷쪽 턴은 앞쪽 턴보다 훨씬 많은 입력 토큰을 소비하며, 대화가 길어질수록 이 격차는 더 벌어집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Prompt Caching은 이 삼각수 효과를 완화하는 핵심 수단입니다.
이전 턴까지의 히스토리는 대부분 캐시에 남아있으므로, 캐시가 적중하면 반복 전송되는 입력 토큰의 비용이 정가의 10%로 줄어듭니다.
캐싱 없이는 뒷쪽 턴의 비용이 급격히 올라가지만, 캐싱이 잘 적중하면 누적 비용 곡선을 크게 낮출 수 있습니다.
캐시가 잘 적중하면 입력 토큰 비용을 90%까지 절약할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;캐시 유형&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설명&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;가격(Input 대비)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cache Write&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;새로운 내용을 캐시에 저장&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;125%&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cache Read&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시된 내용을 재사용&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;10%&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Cache Miss&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;캐시 만료 후 다시 저장&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;125% (Cache Write와 동일)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시에는 TTL(유효 시간)이 있습니다. API의 기본값은 5분이고, 쓰기 비용을 200%로 내는 대신 TTL을 1시간으로 늘리는 옵션도 있습니다.
실제 적용되는 TTL은 사용하는 Plan이나 실행 환경에 따라 다를 수 있으며, 이 글에서는 5분을 예시로 들었습니다.
자세한 내용은 &lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching&quot;&gt;Prompt Caching 공식 문서&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시 히트율을 높이기 위해서 다음을 의식해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;캐시 TTL 안에 다음 턴 진행&lt;/strong&gt;: 입력을 보내고 나서 다음 턴까지 TTL(예: 5분) 이내로 유지하면 캐시가 살아있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;세션 중간에 모델을 전환하지 않기&lt;/strong&gt;: 모델을 바꾸면 캐시가 완전히 초기화됩니다. 하나의 대화 세션에서는 가능하면 하나의 모델을 유지합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;가급적 대화 중 CLAUDE.md를 수정하지 않기&lt;/strong&gt;: CLAUDE.md는 시스템 프롬프트의 일부로 매 턴 전송됩니다. 대화 중에 이 파일을 수정하면 시스템 프롬프트의 prefix가 달라져 기존의 모든 캐시가 무효화됩니다. 특히 턴이 많이 진행된 세션에서는 삼각수 효과로 쌓인 대량의 히스토리 토큰이 전부 Cache Write(125%)로 재과금되어 비용 타격이 큽니다. 수정이 필요하다면 새 세션에서 시작하는 것이 비용 면에서 유리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;직접_시뮬레이션해보기&quot;&gt;직접 시뮬레이션해보기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 내용을 바탕으로 자신의 사용 패턴에 맞는 비용을 직접 계산해볼 수 있는 시뮬레이터를 Claude Code로 만들었습니다.
세션 내의 턴 수, 토큰량, 캐시 히트율 등을 조절하면서 모델별 비용 차이와 누적 비용 곡선을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;claude-code-cost-simulation.html&quot;&gt;Claude Code Token Cost Simulator로 이동 →&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/costs&quot;&gt;Claude Code Costs - Anthropic Docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching&quot;&gt;Prompt Caching - Anthropic Docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/about-claude/models&quot;&gt;Models &amp;amp; Pricing - Anthropic Docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Ubuntu에서의 OEM 커널 설치</title>
      <link>https://blog.benelog.net//oem-kernel.html</link>
      <pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">oem-kernel.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#ubuntu_설치_시점에_따른_경험_차이&quot;&gt;Ubuntu 설치 시점에 따른 경험 차이&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#oem_커널_vs_hwe_커널&quot;&gt;OEM 커널 vs HWE 커널&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#oem_커널이_자동으로_선택되는_원리&quot;&gt;OEM 커널이 자동으로 선택되는 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#oem_커널을_수동으로_설치제거&quot;&gt;OEM 커널을 수동으로 설치/제거&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ubuntu 공식 홈페이지에서 ISO를 받아 같은 방법으로 설치했음에도 설치 시점에 따라서 Lenovo ThinkPad에는 OEM 커널이, Dell XPS에는 HWE 커널이 설치되는 경우를 경험했습니다.
이 글에서는 두 커널의 차이와 자동 선택되는 원리 등을 정리합니다.
현상의 분석과 이 글의 작성에는 Claude Code의 도움을 받았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ubuntu_설치_시점에_따른_경험_차이&quot;&gt;Ubuntu 설치 시점에 따른 경험 차이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 Lenovo와 Dell 2개의 모델에  Ubuntu를 설치해서 사용하고 있습니다.
2024년 11월에 Dell XPS 13 9350 모델에 Ubuntu 24.04를 설치했을 때는 웹캠이 제대로 동작하지 않았습니다.
2025년 8월경 Lenovo ThinkPad X1 Carbon Gen 13에 Ubuntu 24.04를 설치했을 때는 웹캠이 바로 잡혔습니다.
이 둘의 차이는 당시 Dell에서는 HWE 커널이 설치된 반면, Lenovo에서는 OEM 커널이 설치되었기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 2월 현재에는 2개의 모델이 모두 Ubuntu 공식 인증기기로 등록되어 있어서 OEM 커널이 설치될 것으로 예상됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ubuntu.com/certified/202407-34214&quot;&gt;Dell XPS 13 9350&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ubuntu.com/certified/202411-36001&quot;&gt;Lenovo ThinkPad X1 Carbon Gen 13 LNL&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;oem_커널_vs_hwe_커널&quot;&gt;OEM 커널 vs HWE 커널&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ubuntu 24.04 LTS에서 최신 하드웨어 지원을 위해 제공하는 커널은 크게 두 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;항목&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;OEM 커널&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;HWE 커널&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;패키지명 예&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;linux-oem-24.04d&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;linux-generic-hwe-24.04&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;커널 접미사 예&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;6.17.0-1011-oem&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;6.17.0-14-generic&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;대상&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;특정 OEM 파트너 기기 (Lenovo, Dell 등)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;일반 사용자 전체&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;포함 패치&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;해당 기기 전용 드라이버·패치 (카메라, 지문인식, 열 관리 등)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 최신 하드웨어 지원&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;관리 주체&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Canonical OEM 팀 + OEM 파트너&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Canonical / Ubuntu 커뮤니티&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;업데이트 경로&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OEM 파트너 저장소 (&lt;code&gt;oem.archive.ubuntu.com&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;기본 Ubuntu 저장소&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HWE(Hardware Enablement) 커널은 LTS 버전에서 새 하드웨어를 지원하기 위해 최신 업스트림 커널을 백포팅한 것입니다.
OEM 커널은 여기에 더해 특정 기기에서만 필요한 패치가 추가로 적용됩니다.
즉, OEM 커널은 하드웨어 제조사와 Canonical이 협력하여, 해당 기기에서 &quot;설치 후 바로 동작(out-of-the-box)&quot;하는 경험을 제공하기 위한 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intel Meteor Lake / Lunar Lake CPU를 탑재한 기기들은 HWE 커널에서 완전히 지원되지 않은 하드웨어가 있었습니다.
대표적인 예가 내장 카메라(MIPI/IPU7)와 일부 지문인식 센서입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;oem_커널이_자동으로_선택되는_원리&quot;&gt;OEM 커널이 자동으로 선택되는 원리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ubuntu 인스톨러(Subiquity)는 설치 중에 &lt;code&gt;ubuntu-drivers&lt;/code&gt; 도구를 호출하여 현재 하드웨어를 감지합니다.
이때 DMI(System Management BIOS) 정보를 읽어 Canonical OEM 파트너 목록과 대조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들어 Lenovo ThinkPad X1 Carbon Gen 13의 경우, 아래와 같은 메타패키지가 매칭됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Package: oem-sutton-dacia-meta
Modaliases: meta(dmi:*bvnLENOVO:bvrN4B*:pvrThinkPad*)
Depends: linux-oem-24.04c, oem-sutton-meta
Ubuntu-Oem-Kernel-Flavour: oem
Description: hardware support for Lenovo ThinkPad X1 2-in-1 G10, X1 Carbon G13&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Modaliases&lt;/code&gt; 필드의 패턴(&lt;code&gt;dmi:*bvnLENOVO:bvrN4B*:pvrThinkPad*&lt;/code&gt;)이 현재 기기의 DMI 정보와 일치하면,
해당 메타패키지와 함께 OEM 커널이 자동으로 설치됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치 흐름을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Ubuntu 설치 중
└─ ubuntu-drivers 가 DMI 정보 확인
   └─ OEM 메타패키지 패턴 매칭
      └─ oem-sutton-dacia-meta 설치
         └─ linux-oem-24.04d 의존성으로 OEM 커널 설치
            └─ → 부팅 커널: 6.17.0-1011-oem&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 커널이 설치될지는 &lt;strong&gt;하드웨어가 Canonical OEM 파트너 목록에 등록되어 있느냐&lt;/strong&gt;에 달려 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음과 같이 Intel Ultra 7 258V CPU를 쓰는 기기라도 차이가 생길 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;기기&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;OEM 메타패키지&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;설치되는 커널&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Lenovo ThinkPad X1 Carbon G13&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;oem-sutton-dacia-meta&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;linux-oem-24.04d&lt;/code&gt; (OEM 커널)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Dell XPS 13 9350&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;oem-somerville-tyrogue-meta&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;linux-oem-24.04d&lt;/code&gt; (OEM 커널)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OEM 파트너 목록에 없는 기기&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;linux-generic-hwe-24.04&lt;/code&gt; (HWE 커널)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 설치 시기와 모델에 따라 어느 커널이 설치 될지가 달랐던 것입니다.
&lt;a href=&quot;https://ubuntu.com/certified&quot;&gt;우분투 공식 인증기기&lt;/a&gt;라면 OEM 커널이 설치시에 바로 감지되어 설치될 가능성이 높습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 명령을 참고로 알아두시면 기기에 대한 정보를 파악하는데 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ubuntu-drivers list-oem&lt;/code&gt; : 기기에 감지된 OEM 메타패키지&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sudo dmidecode -s system-product-name&lt;/code&gt; :  기기의 정확한 모델명&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;oem_커널을_수동으로_설치제거&quot;&gt;OEM 커널을 수동으로 설치/제거&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 어떤 커널이 설치되어 있는지는 다음 명령으로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;설치된 커널과 OEM 메타패키지 확인&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;# 현재 부팅된 커널
uname -r

# 설치된 커널 패키지 목록
dpkg -l | grep linux-image | grep &apos;^ii&apos;

# ubuntu-drivers가 감지하는 OEM 메타패키지 확인
ubuntu-drivers list-oem&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우분투 초기 설치 시점에 HWE 커널이 자동 선택되었지만, OEM 커널로 전환을 하고 싶다면 아래와 같이 수동으로 설치할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;OEM 커널 수동 설치&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;# 설치 가능한 OEM 메타패키지 목록 검색
apt search linux-oem-24.04

# OEM 커널 수동 설치 (24.04 기준)
sudo apt install linux-oem-24.04d&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞으로 업데이트에도 HWE 커널이 설치되지 않도록 하려면, HWE 커널의 메타 패키지를 제거하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;HWE 커널 제거&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo apt remove linux-headers-generic-hwe-24.04&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또는 HWE 커널의 메타 패키지를 보류(hold) 상태로 설정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;HWE 커널 보류 설정&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo apt-mark hold linux-headers-generic-hwe-24.04&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반대로 OEM 커널 없이 HWE 커널만 사용하고 싶다면, OEM 메타패키지를 제거하면 됩니다.
단, 해당 하드웨어에서 필요한 드라이버가 빠질 수 있으므로 주의가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.ubuntu.com/Kernel/OEMKernel&quot;&gt;Ubuntu OEM Kernel — Ubuntu Wiki&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://ubuntu.com/certified&quot;&gt;Ubuntu Certified Hardware&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.ubuntu.com/IntelMIPICamera&quot;&gt;Intel MIPI Camera — Ubuntu Wiki&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Ubuntu 24.04 설치</title>
      <link>https://blog.benelog.net//installing-ubuntu-24.04.html</link>
      <pubDate>Thu, 9 Oct 2025 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">installing-ubuntu-24.04.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;새 컴퓨터를 받아서 설치한 프로그램들을 정리해봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ubuntu는 영문판으로 설치&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Desktop&lt;/code&gt;, &lt;code&gt;Download&lt;/code&gt; 같은 디렉터리가 영문으로 나오는 것이 편리해서&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;한글_설정&quot;&gt;한글 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo apt install ibus-hangul&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Settings&lt;/code&gt; 에서&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Regision &amp;amp; Language&lt;/code&gt; 에서 &lt;code&gt;Manage Installed Lanauges&lt;/code&gt; 에 &apos;Korean&apos; 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Keyboard&lt;/code&gt; &amp;gt; &lt;code&gt;Input Sources&lt;/code&gt; 에 &lt;code&gt;Korean(Hangul)&lt;/code&gt; 추가하고 &quot;&amp;#8230;&amp;#8203;&quot; 눌러서 &lt;code&gt;Preference&lt;/code&gt; 선택&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Hangul toggle key&lt;/code&gt; 에 &lt;code&gt;Shift + Space&lt;/code&gt; 지정&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;하드웨어_설정&quot;&gt;하드웨어 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;키보드_설정&quot;&gt;키보드 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일부 키보드의 경우 F1~F12가 잘 인식되지 않은 경우도 있음. 해당 장비가 애플 키보드 타입으로 인식되고 자동 모드가 Fn가 기본적으로 눌러져있는 것으로 인식되기 때문. hid_apple 모듈의 fnmode 변수 설정으로 이 문제를 해결할 수 있음.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;현재 모드 확인 : &lt;code&gt;lsmod | grep hid_&lt;/code&gt; (디폴트는 자동 모드인 3)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;보통 2로 모드 설정을 하면 잘 되고, 그래도 안되면 disable인 3으로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;일시적으로 설정 값이 변경하여 테스트 : &lt;code&gt;echo 2 | sudo tee /sys/module/hid_apple/parameters/fnmode&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;키보드 펑션키 모드 변경이 재부팅 후에도 적용되도록&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;echo &quot;options hid_apple fnmode=2&quot; | sudo tee /etc/modprobe.d/hid_apple.conf
sudo update-initramfs -u&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고자료&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sunyzero.tistory.com/290&quot; class=&quot;bare&quot;&gt;https://sunyzero.tistory.com/290&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://mathncode.tistory.com/25&quot; class=&quot;bare&quot;&gt;https://mathncode.tistory.com/25&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;웹_캠_설정&quot;&gt;웹 캠 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intel Ultra 7 258v CPU를 쓰는 Dell XPS 13 plus 9350에서는 웹캠을 위한 드라이버 설치를 따로 해줘야했음.
같은 CPU를 쓰는 Lenovo Thinkpad X1 Carbon G13는 기본 설치만으로 잘 되었음.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dell XPS 13 plus 9350에서 웹캠이 안 잡힐 때 참고한 자료&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.ubuntu.com/IntelMIPICamera&quot; class=&quot;bare&quot;&gt;https://wiki.ubuntu.com/IntelMIPICamera&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.ubuntu.com/Dell&quot; class=&quot;bare&quot;&gt;https://wiki.ubuntu.com/Dell&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;cli_도구&quot;&gt;CLI 도구&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;terminator&quot;&gt;Terminator&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 창을 띄우기에 편한 터미널 프로그램&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo apt install terminator&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;curl&quot;&gt;curl&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo apt install curl&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;ifconfig&quot;&gt;ifconfig&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo apt install net-tools&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;ssh_설정&quot;&gt;ssh 설정&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo vi /etc/ssh/ssh_config&lt;/code&gt; 하여 아래를 마지막에 추가&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;HostkeyAlgorithms ssh-dss,ssh-rsa
KexAlgorithms +diffie-hellman-group1-sha1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://kr.analysisman.com/2018/08/linux-ssh.html&quot; class=&quot;bare&quot;&gt;https://kr.analysisman.com/2018/08/linux-ssh.html&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;git_gitk&quot;&gt;git / gitk&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo apt-get install git gitk&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;github_ssh_설정&quot;&gt;GitHub SSH 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&quot; class=&quot;bare&quot;&gt;https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;oh_myzsh&quot;&gt;oh-myzsh&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo apt install zsh
curl -L http://install.ohmyz.sh | sh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( &lt;a href=&quot;https://github.com/robbyrussell/oh-my-zsh&quot; class=&quot;bare&quot;&gt;https://github.com/robbyrussell/oh-my-zsh&lt;/a&gt; 참고 )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;terminator의 &lt;code&gt;Preference&lt;/code&gt; &amp;gt; &lt;code&gt;Profile&lt;/code&gt; &amp;gt; &lt;code&gt;Command&lt;/code&gt; 에서&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Run a custom command instead of my shell&lt;/code&gt; 체크&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Custom commands&lt;/code&gt; : &lt;code&gt;zsh&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;scm_breeze&quot;&gt;scm_breeze&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;zsh에서 실행할 때의 기준&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;git clone https://github.com/scmbreeze/scm_breeze.git ~/.scm_breeze
~/.scm_breeze/install.sh
source &quot;${ZDOTDIR:-$HOME}/.zshrc
bash
source ~/.bashrc
exit&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( &lt;a href=&quot;https://github.com/ndbroadbent/scm_breeze&quot; class=&quot;bare&quot;&gt;https://github.com/ndbroadbent/scm_breeze&lt;/a&gt; 참고 )&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;개발_환경&quot;&gt;개발 환경&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 JDK를 설치하고 관리하는 방법은 SDKMAN + direnv를 추천 ( &lt;a href=&quot;https://blog.benelog.net/installing-jdk.html&quot; class=&quot;bare&quot;&gt;https://blog.benelog.net/installing-jdk.html&lt;/a&gt; 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;sdkman&quot;&gt;SDKMAN&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl -s &quot;https://get.sdkman.io&quot; | bash
source &quot;$HOME/.sdkman/bin/sdkman-init.sh&quot;
sdk install java&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://sdkman.io/install&quot; class=&quot;bare&quot;&gt;https://sdkman.io/install&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;direnv&quot;&gt;direnv&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sudo apt install direnv&lt;/code&gt; 설치&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;~/.bashrc&lt;/code&gt; 와 &lt;code&gt;~/.zshrc&lt;/code&gt; 에 &lt;code&gt;eval &quot;$(direnv hook bash)&quot;&lt;/code&gt; 추가&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://direnv.net/docs/hook.html&quot; class=&quot;bare&quot;&gt;https://direnv.net/docs/hook.html&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;intellij&quot;&gt;IntelliJ&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo apt install fuse&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/ko-kr/toolbox-app/&quot; class=&quot;bare&quot;&gt;https://www.jetbrains.com/ko-kr/toolbox-app/&lt;/a&gt; 에서 Toolbox로 설치&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Terminal에서 zsh 지정&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Tools&lt;/code&gt; &amp;gt; &lt;code&gt;Application Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Shell Path&lt;/code&gt; : &lt;code&gt;/bin/zsh&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Plugin 설치&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/7391-asciidoc&quot; class=&quot;bare&quot;&gt;https://plugins.jetbrains.com/plugin/7391-asciidoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/1065-checkstyle-idea&quot; class=&quot;bare&quot;&gt;https://plugins.jetbrains.com/plugin/1065-checkstyle-idea&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;node_js&quot;&gt;Node.js&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;nvm_설치&quot;&gt;nvm 설치&lt;/h4&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;~/.bash_profile, ~/.zshrc, ~/.profile,  ~/.bashrc 등에 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;export NVM_DIR=&quot;$([ -z &quot;${XDG_CONFIG_HOME-}&quot; ] &amp;amp;&amp;amp; printf %s &quot;${HOME}/.nvm&quot; || printf %s &quot;${XDG_CONFIG_HOME}/nvm&quot;)&quot;
[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; \. &quot;$NVM_DIR/nvm.sh&quot; # This loads nvm&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nvm-sh/nvm#installing-and-updating&quot; class=&quot;bare&quot;&gt;https://github.com/nvm-sh/nvm#installing-and-updating&lt;/a&gt; 참고&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정보_관리_프로그램&quot;&gt;정보 관리 프로그램&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;크롬&quot;&gt;크롬&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install ./google-chrome-stable_current_amd64.deb&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;obsidian&quot;&gt;Obsidian&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;snap으로 설치하면 한글 문제가 있음. Appimage로 설치&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;pinta&quot;&gt;Pinta&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;snap install pinta&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;vidcutter&quot;&gt;VidCutter&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;snap install vidcutter&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ozmartian/vidcutter&quot; class=&quot;bare&quot;&gt;https://github.com/ozmartian/vidcutter&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;shotcut&quot;&gt;ShotCut&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;snap install shotcut --classic&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://shotcut.org/&quot; class=&quot;bare&quot;&gt;https://shotcut.org/&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;기타_참고&quot;&gt;기타 참고&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이전 버전에서 참고한 자료&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;무선_인터넷_안_잡힐_때&quot;&gt;무선 인터넷 안 잡힐 때&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rfkill list all&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://askubuntu.com/questions/380586/wireless-is-disabled-by-hardware-switch-wifi-doesnt-work-hard-blocked-yes&quot; class=&quot;bare&quot;&gt;http://askubuntu.com/questions/380586/wireless-is-disabled-by-hardware-switch-wifi-doesnt-work-hard-blocked-yes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://askubuntu.com/questions/847250/new-ubuntu-16-04-with-no-wifi/847282&quot; class=&quot;bare&quot;&gt;http://askubuntu.com/questions/847250/new-ubuntu-16-04-with-no-wifi/847282&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;nginx_설치_후_자동으로_시작_안하게&quot;&gt;NGINX 설치 후 자동으로 시작 안하게&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sudo update-rc.d -f nginx disable&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;secure_boot&quot;&gt;Secure Boot&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.ubuntu.com/UEFI/SecureBoot&quot; class=&quot;bare&quot;&gt;https://wiki.ubuntu.com/UEFI/SecureBoot&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>카멜 케이스를 쓸 때 약어 표기</title>
      <link>https://blog.benelog.net//acronym-camel-case.html</link>
      <pubDate>Sat, 2 Dec 2023 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">acronym-camel-case.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이 글에서는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Camel_case&quot;&gt;위키페디아의 정의에 따라&lt;/a&gt; &apos;카멜 케이스&amp;#8217;를 UpperCamelCase와 lowerCamelCase를 포괄하는 개념으로 사용했습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자바나 코틀린에서 약어(acronym)가 클래스명이나 메서드명의 일부로 들어갈 때 최대 두 글자까지만 대문자로 표기하는 방식을 권장합니다.
비슷하게 카멜 케이스를 쓰는 자바스크립트와 같은 다른 언어에서도 이 관례가 무난하다고 생각합니다.
기존의 바꿀 수 없는 코드나 외부 라이브러리 사용 등으로 어쩔 수 없는 경우에는 전부 대문자로 쓰더라도 이를 최소화하는 노력을 하고 프로젝트 내부 문서로도 예외의 경우로 명시하면 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 관례는 약어 전체를 대문자로 표기하는 관례에 비해서 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;연속된 약어가 조합될 경우 단어 사이의 구문이 더 잘 인지됩니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;예: &lt;code&gt;XMLRPCAPIURL&lt;/code&gt; vs &lt;code&gt;XmlRpcApiUrl&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;일관성을 유지하기가 더 쉽습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;전부 대문자로 표기해야할 단어들을 따로 기억할 필요가 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;사람마다 다르게 판단할 여지가 적은 단순한 규칙입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;영향력이 큰 책이나 가이드에서 지지받고 있는 규칙입니다. 새로 합류한 구성원도 따를 가능성이 높습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자바와 코틀린으로 나누어서 이 관례에 대해 참고할 수 있는 자료를 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;자바의_관례&quot;&gt;자바의 관례&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 내부에서는 이 관례가 통일되어 있지 않습니다.
다음과 같이 약어를 쓸 때 전부 대문자와 첫 글자만 대문자 표기가 혼재하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;java.net.HttpURLConnection (한 클래스 이름에서도 Http는 첫글자만 대문자, URL은 다 대문자로 표기했습니다.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;javax.xml.bind.annotation.XmlElement&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Effective Java의 3판에서는 약어라도 첫글자만 대문자로 하는 방식에 대한 지지도를 더 높인 느낌입니다. 2판의 &lt;code&gt;Upppercase may be more common&lt;/code&gt; 문구가 `Some programmers still use uppercase`로 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;:&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/effective-java/camel-casing.png&quot; alt=&quot;camel-casing&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3판에서의 해당 단락의 전체 내용은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Item 68: Adhere to generally accepted naming conventions (원서 289~290 페이지)&lt;/div&gt;
&lt;blockquote&gt;
There is some disagreements as to whether acronyms should be uppercase or have only their first letter capitalized. While some programmers stil use uppercase, a strong argument can be made in favor of capitalizing only the first letter: even if multiple acronyms occur back-to-back, you can still tell where one word starts and the next word ends. Which class name would you rather see, HTTPURL or HttpUrl?
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;아이템 68: 일반적으로 통용되는 명명 규칙을 따르라 (번역서 382 페이지)&lt;/div&gt;
&lt;blockquote&gt;
약자의 경우 첫 글자만 대문자로 할지 전체를 대문자로 할지는 살짝 논란이 있다. 전체를 대문자로 쓰는 프로그래머도 있지만, 그래도 첫 글자만 대문자로 하는 쪽이 훨씬 많다. HttpUrl처럼 여러 약자가 혼합된 경우라도 각 약자의 시작과 끝을 명확히 알 수 있기 때문이다. (HTTPURL과 비교해보자)
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자바 초창기에 비해 점점 첫 글자만 대문자로 쓰는 점유율이 높아졌다고 해석될만도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링 프레임워크에서도 초창기에는 두 방식이 혼재하다가 점점 첫글자만 대문자로 쓰는 관례로 바뀐 것으로 보입니다.
URL 단어 표기는 다음과 같은 예시가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URLEditor.java&quot;&gt;URLEditor&lt;/a&gt; : 2002년 작성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/io/UrlResource.java&quot;&gt;UrlResource&lt;/a&gt; : 2003년 작성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java&quot;&gt;UrlTag&lt;/a&gt; : 2009년 작성 추정&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DAO, XML, SQL 등은 모두 처음부터 첫글자만 대문자로 표기된 것으로 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java&quot;&gt;JdbcDaoSupport&lt;/a&gt; : 2003년 처음 작성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java&quot;&gt;SqlXmlValue&lt;/a&gt; : 2008년 작성 추정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-oxm/src/main/java/org/springframework/oxm/XmlMappingException.java&quot;&gt;XmlMappingException&lt;/a&gt; : 2009년 작성 추정&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;코틀린의_관례&quot;&gt;코틀린의 관례&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코틀린 언어 사이트의 코딩 컨벤션 가이드에서도 약어의 표기 방식에 언급하고 있습니다.
약어가 두 글자라면 다 대문자로 쓰고, 길어지면 한 글자만 대문자로 쓰라고 권장하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coding-conventions.html#choose-good-names&quot; class=&quot;bare&quot;&gt;https://kotlinlang.org/docs/coding-conventions.html#choose-good-names&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Choose Good names&lt;/div&gt;
&lt;blockquote&gt;
When using an acronym as part of a declaration name, capitalize it if it consists of two letters (IOStream); capitalize only the first letter if it is longer (XmlFormatter, HttpInputStream).
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;좋은 이름을 선택해라&lt;/div&gt;
&lt;blockquote&gt;
선언된 이름의 일부에 약어가 들어 갈 때는 두 개의 문자로 구성된 약어는 대문자로 쓰고(IOStream) 긴 문자는 첫 글자만 대소문자로 쓰세요. (XmlFormatter, HttpInputStream).
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;언어 공식 사이트의 가이드에 있는 내용이므로 보편적으로 인지되고 유지될 가능성이 높은 관례입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>필독 개발자 온보딩 가이드</title>
      <link>https://blog.benelog.net//missing-readme.html</link>
      <pubDate>Thu, 23 Nov 2023 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">missing-readme.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#감상&quot;&gt;감상&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#의견_메모&quot;&gt;의견 메모&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#추천자료&quot;&gt;추천자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://image.yes24.com/goods/119108069/XL&quot; alt=&quot;Cover&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;이미지 출처 : &lt;a href=&quot;https://www.yes24.com/Product/Goods/119108069&quot; class=&quot;bare&quot;&gt;https://www.yes24.com/Product/Goods/119108069&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;각 장의 참고문헌 목록을 옮겨 정리하는 것에 대해서는 책만 출판사(&lt;a href=&quot;https://www.onlybook.co.kr)에&quot; class=&quot;bare&quot;&gt;https://www.onlybook.co.kr)에&lt;/a&gt; 연락해서 게재 허락을 구했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;감상&quot;&gt;감상&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(내돈내산 후기입니다)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많지 않은 분량으로 실무 개발자의 폭넓은 업무를 다루면서 결코 얄팍하지 않은 지식을 전달하는 책입니다.
협업, 설계, 구현, 테스트, 문서화, 긴급 대응, 경력 관리 등 다양한 분야에 대한 알찬 조언을 책 한권에 눌러 담았고 더 많은 분량을 학습하고 싶은 독자를 위한 참고자료들도 매 장마다 소개하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책은 신입 개발자를 대상으로 한 책으로 홍보되고 있지만 경력이 많은 개발자가 읽을만한 가치도 충분합니다. 저도 과거의 경험을 돌아보면서 생각을 정리할 수 있었습니다. 스스로 절실하게 얻은 교훈이 이 책에도 적혀 있는 보면 반가웠고 깊은 공감이 되는 문장들도 많이 만날수 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들면, Dark launching이나 Feature toggle 같이 제 경험에서 유용했던 기법이 개발 실무의 액기스를 전하는 이 책에서도 소개되니 반갑고 안심이 되었습니다. 긴급 대응을 위한 당번(on call) 제도와 같이 더 개선을 해야 할 요소도 독서 후에 더 크게 느껴지기도 했습니다. 전에는 썼지만 근래에는 활용하지 않았던 Liquibase를 통한 DB 스키마 관리도 다음 프로젝트에서는 다시 시도해봐야겠다는 생각도 듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;유용하다고 느낀 지침과 조언을 새겨두고 실천하기 위해 여러 번 읽어야겠다는 생각을 하면서 마지막 장을 덮었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;의견_메모&quot;&gt;의견 메모&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;책을 보다가 의견을 메모한 부분을 옮겨봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p102&quot;&gt;p102&lt;/h3&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예외는 일찍 던지고 최대한 나중에 처리하자.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최대한 나중에 처리하는 것보다는 적절한 추상화 레이어에서 처리하자는 것이 좋다고 생각한다. 예를 들면 사용자의 ID로 조회했을 때 DB 조회가 실패했을 때 생기는 `EmptyResultDataAccessException`을
애플리케이션 콜스택의 마지막에서 처리하기보다는 비지니스 로직을 다루는 레이어에서 `UserNotFoundException`과 같이 비지니스 맥락의 의미가 부여된 예외로 바꾸는 것이 좋을 때도 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p204&quot;&gt;p204&lt;/h3&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기능 브랜치 기반 개발은 트렁크의 코드가 사용자에게 릴르스하기엔 너무 불안정해서 트렁크를 안정화시키는 동안 개발자가 기능 개발을 수행할 수 없는 경우에 사용하는 방법이다. 고객이 각자 다른 버전의 소프트웨어를
사용하는 경우에 보편적으로 기능 브랜치 기반 개발 전략을 채택한다. 서비스 지향 시스템(service-oriented systems)은 일반적으로 트렁크 기반 개발 전략을 채택한다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 보편적인 기능 브랜치 전략은 2010년 빈센트 드리센이 소개한 깃플로(Gitflow)라고 부르는 전략이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git-flow는 필요에 따라 팀 브랜치 정책의 종착역은 될수 있지만
시작점으로 권장하고 싶지는 않다. 자세한 의견은
&lt;a href=&quot;https://blog.benelog.net/rethink-about-git-flow&quot;&gt;Git-Flow에 대해서 다시
생각해보기&lt;/a&gt;라는 글로 따로 정리했다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p212&quot;&gt;p212&lt;/h3&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버전 제어 시스템을 릴리스 리포지토리처럼 사용할 수는 있지만 원래
용도라고 보기 어렵다. 버전 제어 시스템에서는 검색이나 배포 관련 기능을
별로 제공하지 않는다. 대규모 배포를 위해 만들어진 시스템이 아니므로 그런
상황에서는 문제를 일으킬 수도 있다. 버전 제어 시스템 머신과 동일한
머신이 개발자 체아크웃, 도구로부터의 요청, 배포 요청을 모두 담당하면
프로덕션 배포에 영향을 미칠 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Node.js나 Go언어의 오픈소스 생태계에서는 GitHub.com을 릴리스
리포지토리처럼 쓰는 관행이 굳어졌다. 하지만 사내에서라면 이를 분리하는
것이 좋다는 저자의 의견에 동의한다. 버전 관리 시스템은 GitHub
Enterprise를 쓰더라도 릴리스 저장소는 Nexus나 Artifactory를 쓰는 식이다.
분리하면 두 시스템을 각각 업그레이드하기에도 유리하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p213&quot;&gt;p213&lt;/h3&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;릴리스는 최대한 자주 수행하자. 릴리스 주기가 늘어지 거짓된 안정감을 심어줄 수 있다. 즉 릴리스 사이의 주지가 길면 변경사항을 테스트할 충분한 시간이 있는 것처럼 느껴지기 때문이다. 실제로 릴리스 주기를 짧게 가져가면 더 안정적인 소프트웨어를 구현할 수 있어 버그가 발견됐을 때 더 쉽게 처리할 수 있다. 매 주기마다 릴리스되는 변경사항의 수가 더 적으므로 각 릴리스의 위험도도 낮아진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;공감한다. 1~2주에 한번 릴리스하는 조직과 한달에 한번 릴리스하는 조직을 비교했을 때 후자의 릴리스가 훨씬 힘겹고 위험한 일이 되는 현상을 몇 번 목격했다. 테스트와 모니터링의 자동화 수준이 높아져야 릴리스의 비용이 적어져서 더 자주 릴리스하게 될 수 있기도하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p212_2&quot;&gt;p212&lt;/h3&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배포를 원자적으로 만드는 가장 쉬운 방법은 기존에 설치된 소프트웨어를 덮어쓰는 것이 아니라 다른 경로에 소프트웨어를 설치하는 것이다. 일단 패키지가 설치된 다음에는 단축 아이콘이나 심볼릭 링크를 이용해 교체하면
된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(의견) 많이 쓰던 기법이다. 사내 배포 시스템에서 디폴트로 제공이
되기도한다. 이동욱 님이 발표한 &lt;a href=&quot;https://youtu.be/_nkJkWVH-mo?t=2452&quot;&gt;우아한스프링배치&lt;/a&gt; 발표에서도 이
기법을 응용한 무중단 배포를 소개하고 있다. 배포된 최신 버전 파일로  심볼릭 링크를 교체하고 (&lt;code&gt;ln -s -f v2.jar app.jar&lt;/code&gt;) readlink 명령어로 그 파일을 실행하는(&lt;code&gt;java -jar ${readlink ./app.jar&lt;/code&gt;}) 이용하는 방식이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p219&quot;&gt;p219&lt;/h3&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한 번에 모조리 새 코드로 전환하는 일은 위험하다. 테스트를 아무리 많이 해도 버그 발생 가능성을 없앨 수는 없으며 한 번에 모든 사용자에게 코드를 롤아웃하면 모두가 동시에 문제를 겪을 수 있다. 따라서 변경 사항을
점진적으로 롤아웃하고 시스템 상태 지표를 모니터링 하는 편이 좋다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;규모가 있는 시스템을 변경하는 개발자라면 절실하게 읽어야할 문장이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;p311&quot;&gt;p311&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;데이터베이스 스키마 마이그레이션 도구인 Liquibase 등을 소개함.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;데이터베이스와 애플리케이션 수명주기를 결합해서는 안 된다. 애플리케이션 배포 과정에서 스키마를 마이그레이션하는 것은 위험하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;의도한 바은 아니지만 JPA의 스키마 관리 기능이 운영환경에서 실행되어서 발생하는 장애를 종종 전해듣는다. 스프링 부트의 &lt;code&gt;spring.jpa.hibernate.ddl-auto&lt;/code&gt; 같은 옵션도 Local PC에 DB가 따로 설치되어 있거나 Embeded DB를 쓸때만 활성화해야 한다고 생각한다. 공용 개발 DB에서부터는 Liquibase와 같은 스키마 관리 도구를 활용하는 것이 좋다. 공용 개발 DB라도 여러 개가 있는 것이 다양한 테스트를 하는데 유리하고, 개발 DB가 여러개가 되면 스키마 관리 도구의 필요성이 높아진다.
Oracle 등 개발자가 여러 인스턴스를 설치하는데 제약이 있는 DB를 오랫동안 써 온 개발자는 이런 도구의 필요성을 느끼거나 적용할 수 있는 기회를  얻기가 어려울 수도 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;운영DB는 DBA가 따로 SQL파일을 실행해서 스키마를 반영하는 회사라도 Liquibase는 충분히 사용 가능하다. Liquibase는 오프라인 지원 기능이 있어서 실제 DB에 스키마 변경을 실행하지 않고 DDL을 파일로 뽑아낼수도 있다. 예를  들어 스키마 버전 1.0.0-RC1과 1.0.0-RC2 버전 사이에 변경된 칼럼을 반영하는 DDL을 담은 .sql파일을 뽑을 때는 아래 명령어로 가능하다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;rm target/release.csv
mvn liquibase:updateSQL -Dliquibase.url=&apos;offline:mysql?changeLogFile=target/release.csv&apos; -Dliquibase.toTag=1.0.0-RC1
mvn liquibase:updateSQL -Dliquibase.url=&apos;offline:mysql?changeLogFile=target/release.csv&apos; -Dliquibase.toTag=1.0.0-RC2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;추천자료&quot;&gt;추천자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책에서는 각 장의 끝무렵에 ’레벨업을 위한 읽을 거리’로 해당 장에서 다룬 주제를 더 깊이 있게 학습하는데 도움이 되는 자료를 추천했습니다.
여러 번 언급되는 구글의 SRE 관련 책들은 &lt;a href=&quot;https://sre.google/books/&quot; class=&quot;bare&quot;&gt;https://sre.google/books/&lt;/a&gt; 에서 무료로 열람도 가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;표기된 제목이 여러가지 이유로 달라진 책들은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[8장] &lt;a href=&quot;https://www.yes24.com/Product/Goods/11406822&quot;&gt;Continuous Delivery: 신뢰할 수 있는 소프트웨어 출시&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;번역판 초판에는 이 책 제목이 `신뢰할 수 있는 소프트웨어 출시&apos; 였으나 중쇄를 하면서 원서와 동일하게 제목이 바뀌었다고
함.(&lt;a href=&quot;https://www.facebook.com/fupfin.geek/posts/pfbid02ksHoMdnvhkJkN9xWS4qQimt541smxX3oscttadaYVMHLxtQ6CZtkKsHEw82LjXs3l?comment_id=328411842995894&quot;&gt;박성철 님의 관련 댓글&lt;/a&gt;) ’개발자 온보딩 가이드’에서는 번역판 초판의 제목으로 표기되어 있어 있음.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[8장] &lt;a href=&quot;https://smartstore.naver.com/yes24book/products/9565539174&quot;&gt;Release의 모든 것 - 대규모 웹 분산 시스템을 위한 운영 고려 설계&lt;/a&gt; (번역서 2판. 2023년 11월 출간)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;번역서 1판 : &lt;a href=&quot;https://www.yes24.com/Product/Goods/2753365&quot;&gt;Release it:성공적인 출시를 위한 소프트웨어 설계와 배치&lt;/a&gt; (2007년 출판) 이 제목으로 책에서는 표기&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[10장] Elements of Style : 영어 글쓰기에 대한 유명한 책으로 번역판도 여러 번 나옴.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001729444&quot;&gt;글쓰기의 요소(삽화판)&lt;/a&gt; (2016년 출판, 장영준 역) : ’개발자 온보딩 가이드’에서 소개한 판인데 현재 품절이라 중고로만 구할 수 있음.
&lt;a href=&quot;https://www.yes24.com/Product/Goods/74241463&quot;&gt;Yes24에서 원서&lt;/a&gt;가 같은 삽화가 들어간 판으로는 배송비 고려하면 가장 싸게 구할 수 있는
경로인 듯함.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://play.google.com/store/books/details/%EC%9C%8C%EB%A6%AC%EC%97%84_%EC%8A%A4%ED%8A%B8%EB%A0%81%ED%81%AC_2%EC%84%B8_%EA%B8%80%EC%93%B0%EA%B8%B0%EC%9D%98_%EC%9A%94%EC%86%8C&quot;&gt;글쓰기의 요소&lt;/a&gt; (2016년 출판. 김영일 역, 전자책)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/2746329&quot;&gt;영어 글쓰기의 기본&lt;/a&gt;
(2007년 출판, 조서연 공역) : 번역서와 원서가 한권으로 묶여서 나온 판이다. 현재도 구매 가능하다. 역자가 쓴 저서인 &lt;a href=&quot;https://www.yes24.com/Product/Goods/4671236&quot;&gt;영어 글쓰기의 기본 2&lt;/a&gt;도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2장_역량을_높이는_의식적_노력&quot;&gt;2장 역량을 높이는 의식적 노력&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/4045732&quot;&gt;프로그래머의 길,멘토에게 묻다&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/95735260&quot;&gt;나는 왜 도와달라는 말을 못할까: 부담은 줄이고 성과는 높이는 부탁의 기술&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;짝 프로그래밍&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/2126201&quot;&gt;익스트림 프로그래밍&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://martinfowler.com/articles/on-pair-programming.html&quot;&gt;On Pair Programming&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;가면 증후군이나 더닝 크루거 효과에 대한 참고자료&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)자존감은 어떻게 시작되는가: 당신의 인생을 결정짓는 자세의 차이&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/36962337&quot;&gt;종이책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://play.google.com/store/books/details/%EC%97%90%EC%9D%B4%EB%AF%B8_%EC%BB%A4%EB%94%94_%EC%9E%90%EC%A1%B4%EA%B0%90%EC%9D%80_%EC%96%B4%EB%96%BB%EA%B2%8C_%EC%8B%9C%EC%9E%91%EB%90%98%EB%8A%94%EA%B0%80?id=IFcxDwAAQBAJ&quot;&gt;전자책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;3장_코드와_함께_춤을_레거시_코드에_임하는_우리의_자세&quot;&gt;3장 코드와 함께 춤을: 레거시 코드에 임하는 우리의 자세&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/64586851&quot;&gt;레거시 코드 활용 전략: 손대기 두려운 낡은 코드, 안전한 변경과 테스트 기법&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.amazon.com/Legacy-Code-Programmers-Toolbox-Professionals/dp/1691064130&quot;&gt;The Legacy Code Programmer’s Toolbox: Practical Skills for Software Professionals Working with Legacy Code&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/89649360&quot;&gt;리팩터링 2판: 코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/16928943&quot;&gt;맨먼스 미신&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;4장_운영_환경을_고려한_코드_작성_개발환경과_프로덕션_환경은_엄연히_다르다&quot;&gt;4장 운영 환경을 고려한 코드 작성: 개발환경과 프로덕션 환경은 엄연히 다르다.&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/44130507&quot;&gt;Code Complete 코드 컴플리트 2: 더 나은 소프트웨어 구현을 위한 실무 지침서&lt;/a&gt; : 8장에서 방어적 프로그래밍에 대해서 다룸.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/11681152&quot;&gt;Clean Code:소프트웨어 장인 정신&lt;/a&gt; : 7장과 8장에서 예외처리와 경계에 대해서 다룸&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://aws.amazon.com/builders-library/&quot;&gt;The Amazon Builders’ Library&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/105804670&quot;&gt;SRE를 위한 시스템 설계와 구축: 구글이 공개하는 SRE 모범 사례와 설계, 구현, 운영 노하우&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/57979286&quot;&gt;사이트 신뢰성 엔지니어링: 구글이 공개하는 서비스 개발과 운영 노하우&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;5장_피할_수_없는_코드_의존성의_관리_복잡한_프로그램을_짜봐야_비로서_깨닫는_의존성이_진실&quot;&gt;5장 피할 수 없는 코드 의존성의 관리: 복잡한 프로그램을 짜봐야 비로서 깨닫는 의존성이 진실&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://semver.org/&quot;&gt;시맨틱 버저닝 스펙&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://peps.python.org/pep-0440/&quot;&gt;PEP 440 – Version Identification and Dependency Specification&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;6장_테스트_개발자의_든든한_지원군&quot;&gt;6장 테스트! 개발자의 든든한 지원군&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/104084175&quot;&gt;단위 테스트:생산성과 품질을 위한 단위 테스트 원칙과 패턴&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/12246033&quot;&gt;테스트 주도 개발&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/107077663&quot;&gt;실용주의 프로그래머&lt;/a&gt;: 속성 기반 테스팅(Property based testing)절 살펴보기&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/14829054&quot;&gt;탐험적 테스팅: 배우고 통찰하며 개선하는 소프트웨어 테스트&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;7장_올바로_주고받는_코드_리뷰_원만한_팀_협업과_높은_코드_품질을_목표로&quot;&gt;7장 올바로 주고받는 코드 리뷰: 원만한 팀 협업과 높은 코드 품질을 목표로&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://google.github.io/eng-practices/review/&quot;&gt;구글의 Code Review Developer Guide&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/14759898&quot;&gt;하버드 피드백의 기술: 밀어붙이는 피드백에서 끌어당기는 피드백으로&lt;/a&gt;
(&lt;a href=&quot;https://play.google.com/store/books/details/%EB%8D%94%EA%B8%80%EB%9F%AC%EC%8A%A4_%EC%8A%A4%ED%86%A4_%EC%89%B4%EB%9D%BC_%ED%9E%8C_%ED%95%98%EB%B2%84%EB%93%9C_%ED%94%BC%EB%93%9C%EB%B0%B1%EC%9D%98_%EA%B8%B0%EC%88%A0?id=6RirBAAAQBAJ&quot;&gt;Google
play 이북&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;8장_고객_앞으로_소프트웨어_전달_마침내_프로덕션_환경에_안착시킬_소프트웨어의_종착지&quot;&gt;8장 고객 앞으로! 소프트웨어 전달: 마침내 프로덕션 환경에 안착시킬 소프트웨어의 종착지&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/33057253&quot;&gt;팀을 위한 Git: Git 워크플로우를 효율적으로 만드는 사용자 중심 접근법&lt;/a&gt; (Yes24 전자책)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/11406822&quot;&gt;Continuous Delivery: 신뢰할 수 있는 소프트웨어 출시&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/57979286&quot;&gt;사이트 신뢰성 엔지니어링: 구글이 공개하는 서비스 개발과 운영 노하우&lt;/a&gt; : 8장에 릴리스 엔지니어링 참조&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://aws.amazon.com/ko/builders-library/&quot;&gt;Amazon Builders&apos; Library&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://smartstore.naver.com/yes24book/products/9565539174&quot;&gt;Release의 모든 것 - 대규모 웹 분산 시스템을 위한 운영 고려 설계&lt;/a&gt; (번역서 2판. 2023년 11월 출간)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;번역서 1판 : &lt;a href=&quot;https://www.yes24.com/Product/Goods/2753365&quot;&gt;Release it:성공적인 출시를 위한 소프트웨어 설계와 배치&lt;/a&gt; (2007년 출판)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;원서 2판 : &lt;a href=&quot;https://www.amazon.com/Release-Design-Deploy-Production-Ready-Software/dp/1680502395/&quot;&gt;Release It!: Design and Deploy Production-Ready Software&lt;/a&gt; (2018년 출간)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;9장_긴급대응_온콜_업무&quot;&gt;9장 긴급대응 온콜 업무&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://increment.com/on-call/when-the-pager-goes-off/&quot;&gt;What happens when the pager goes off?&lt;/a&gt; : 책에서 발췌한 장애 대응 5단계&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/57979286&quot;&gt;사이트 신뢰성 엔지니어링: 구글이 공개하는 서비스 개발과 운영 노하우&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;4장: SLI, SLO를 직접 정의해야할 때 참조&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;11장, 13장, 14장, 15장 : 온콜, 비상 대기, 장애 처리, 포스트터모텀&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;10장_견고한_소프트웨어를_위한_기술_설계_절차&quot;&gt;10장 견고한 소프트웨어를 위한 기술 설계 절차&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(동영상)&lt;a href=&quot;https://www.youtube.com/watch?v=f84n5oFoZBc&quot;&gt;Hammock Driven Development - Rich Hickey&lt;/a&gt; : 복잡한 소프트웨어 설계 과정을 가장 잘 설명한 자료라고 소개됨.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)대규모 오픈소스 프로젝트의 설계 과정&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/python/peps&quot;&gt;파이썬 개선 제안(PEP)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/kafka/kafka+improvement+proposals&quot;&gt;카프카
개선 제안(KIP)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs&quot;&gt;러스트 검토 요청(RFCs)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://wecode.wepay.com/posts/effective-software-design-documents&quot;&gt;Effective Software Design Documents&lt;/a&gt; : 위페이가 설계를 수행하는 방법과 그 방법이 어떻게 개선돼왔는지를 설명&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/wepay/design_doc_template&quot;&gt;설계 문서 템플릿&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)Elements of Style&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001729444&quot;&gt;글쓰기의 요소(삽화판)&lt;/a&gt; (2016년 출판, 장영준 역)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://play.google.com/store/books/details/%EC%9C%8C%EB%A6%AC%EC%97%84_%EC%8A%A4%ED%8A%B8%EB%A0%81%ED%81%AC_2%EC%84%B8_%EA%B8%80%EC%93%B0%EA%B8%B0%EC%9D%98_%EC%9A%94%EC%86%8C&quot;&gt;글쓰기의 요소&lt;/a&gt; (2016년 출판. 김영일 역, 전자책)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/2746329&quot;&gt;영어 글쓰기의 기본&lt;/a&gt;
(2007년 출판, 조서연 공역) : 번역서와 원서가 한권으로 묶여서 나온 판&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/2774464&quot;&gt;글쓰기 생각쓰기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)폴 그레이엄(Paul Grahm)의 글쓰기에 대한 기고&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.paulgraham.com/useful.html&quot;&gt;How to Write Usefully&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.paulgraham.com/talk.html&quot;&gt;Write Like You Talk&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;11장_소프트웨어_수명주기를_고려한_진화하는_아키텍처_구현&quot;&gt;11장 소프트웨어 수명주기를 고려한 진화하는 아키텍처 구현&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)진화적 아키텍처: 피트니스 함수, 거버넌스 자동화를 활용해 생산성 높은 소프트웨어 구축하기&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/121961003&quot;&gt;종이책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://play.google.com/store/books/details/%EB%8B%90_%ED%8F%AC%EB%93%9C_%EC%A7%84%ED%99%94%EC%A0%81_%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98?id=6w_UEAAAQBAJ&quot;&gt;전자책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`필독! 개발자 온보딩 가이드&apos; 본문에서는 원서가 소개되어 있는데, 2023년 8월에 번역판이 출판되었다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/25100510&quot;&gt;도메인 주도 설계 구현: Implementing Domain-Driven Design&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.amazon.com/Philosophy-Software-Design-2nd-ebook/dp/B09B8LFKQL/&quot;&gt;A Philosophy of Software Design, 2ED&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.amazon.com/Elements-Clojure-Zachary-Tellman/dp/0359360580/&quot;&gt;Elements of Clojure&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(동영상)(&lt;a href=&quot;https://www.youtube.com/watch?v=SxdOUGdseq4)&quot;&gt;Simple Made Easy - Rich Hickey
(2011)&lt;/a&gt; : 간결성, 복잡성, 손쉬움, 좋은 소프트웨어를 구현하는 방법에 대해서 설명&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.amazon.com/Data-Mesh-Zhamak-Dehghani-ebook/dp/B09V4KWWJ8/&quot;&gt;Data Mesh&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/595ref=sr_1_1?crid=3UIMYGOHHDTLL&amp;amp;keywords=data+mesh&amp;amp;qid=1698069866&amp;amp;sprefix=data+me%2Caps%2C261&amp;amp;sr=8-166585&quot;&gt;데이터 중심 애플리케이션 설계: 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운
시스템을 지탱하는 핵심 아이디어&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;12장_효율적인_협업을_위한_애자일_문화&quot;&gt;12장 효율적인 협업을 위한 애자일 문화&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://agilemanifesto.org/principles.html&quot;&gt;Principles behind the Agile Manifesto&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(웹)&lt;a href=&quot;https://www.atlassian.com/agile&quot;&gt;Atlassian의 애자일 문서&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;13장_관리자_팀장_상사와_함께_일하기&quot;&gt;13장 관리자, 팀장, 상사와 함께 일하기&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(책)개발 7년차, 매니저 1일차: 개발만 해왔던 내가, 어느 날 갑자기 ‘팀’을 맡았다!&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/87336637&quot;&gt;종이책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://play.google.com/store/books/details/%EC%B9%B4%EB%AF%B8%EC%9C%A0_%ED%91%B8%EB%A5%B4%EB%8B%88%EC%97%90_%EA%B0%9C%EB%B0%9C_7%EB%85%84%EC%B0%A8_%EB%A7%A4%EB%8B%88%EC%A0%80_1%EC%9D%BC%EC%B0%A8?id=Qbf_DwAAQBAJ&quot;&gt;전자책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.amazon.com/Elegant-Puzzle-Systems-Engineering-Management-ebook/dp/B07QYCHJ7V/&quot;&gt;An
Elegant Puzzle: Systems of Engineering Management&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)일의 99%는 피드백이다: 하버드 협상연구소에서 알려주는 대화의 기술&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/102003851&quot;&gt;종이책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://play.google.com/store/books/details/%EB%8D%94%EA%B8%80%EB%9F%AC%EC%8A%A4_%EC%8A%A4%ED%86%A4_%EC%89%B4%EB%9D%BC_%ED%9E%8C_%EC%9D%BC%EC%9D%98_99_%EB%8A%94_%ED%94%BC%EB%93%9C%EB%B0%B1%EC%9D%B4%EB%8B%A4?id=UccyEAAAQBAJ&quot;&gt;전자책&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.amazon.com/Managing-Up-Move-Work-Succeed-ebook/dp/B07BB4QFDF/&quot;&gt;Managing
Up: How to Move up, Win at Work, and Succeed with Any Type of Boss&lt;/a&gt; :상사나 관리자의 개성을 어떻게 고려해야하는지, 엄격한 관리자에 대응하는 법, 이직하는 방법 등을 다룸.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(책)&lt;a href=&quot;https://www.yes24.com/Product/Goods/61333181&quot;&gt;하이 아웃풋 매니지먼트: 어떻게 성과를 높일 것인가&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Git-Flow에 대해서 다시 생각해보기</title>
      <link>https://blog.benelog.net//rethink-about-git-flow.html</link>
      <pubDate>Mon, 23 Oct 2023 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">rethink-about-git-flow.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git의 브랜치 전략으로 Git-flow가 가장 유명하고 자주 언급됩니다.
그런데 이 전략은 복잡도와 실행하는 난이도가 높은 편입니다.
Git-flow를 쓰고 있다고 주장하는 조직에서도 창시자 Vincent가 제안한 원래의 프로세스를 그대로 쓰는 경우는 드물다고 느껴집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git-flow가 특히 인터넷 서비스 개발에서 보편적으로 권장할만한 전략일지는 저는 의문입니다.
브랜치 전략은 복잡하게 시작해서 단순하게 줄여나가기 보다는 단순하게 시작해서 필요에 따라 복잡도를 늘여가는 편이 시행착오가 적다고 생각합니다.
여러 버전을 동시에 운영하고 백포트 패치를 해야하는 솔루션 개발에서는 Git-flow가 적합한 조직이 있을법도 합니다.
즉, Git-flow는 필요에 따라 종착역은 될수 있지만 시작점으로 권장하고 싶은 정책은 아닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git-flow가 그 이름 때문에 필요 이상으로 권장되고 있다고 저는 생각합니다. Git이 지금보다 대중화되지 않았을 때 제안된 모델이고 그 당시 다른 유명한 모델이 없겠기에 Git-flow라고 불리지 않았을까 추정하기도합니다. 만약 창시자의 이름을 딴 Vincent-flow정도의 이름이였다면 많은 사람들이 따르려고 생각하지 않았을지도 모릅니다. Git-flow가 유용해보인다면 그 이름이 Vincent-flow라도 마찬가지일지를 한번 돌아봐도 이름에 과도하게 끌린건 아닌지 돌아보는데 도움이 될듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;뱅크 샐러드에서는 Git-flow보다 더 단순한 전략으로 리뷰-배포 프로세스를 개선했다는 사례도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.banksalad.com/tech/become-an-organization-that-deploys-1000-times-a-day/&quot; class=&quot;bare&quot;&gt;https://blog.banksalad.com/tech/become-an-organization-that-deploys-1000-times-a-day/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;먼저 Git-flow를 활용한다면 하나의 기능을 배포하는 데 있어서 무려 5번의 branch switching이 필요하고, 6번의 Pull Request와 이에 따른 6번의 Code Review가 필요했습니다. 이 말은 다르게 말하면 코드 오너가 6번이나 코드를 리뷰하고 승인을 해줘야 한다는 것이죠. 이렇게 복잡한 프로세스는 자연스럽게 배포를 귀찮은 존재로 만듭니다. 이로 인해 간단한 수정 사항의 경우 develop branch에 merge 한 뒤 배포하지 않는 일이 종종 발생하기 시작했습니다. 이런 배포되지 않은 변경 사항이 쌓이면서 나중에 필요할 때 한 번에 너무 많은 변경사항을 포함한, 부담스러운 배포를 진행해야만 했습니다. 배포 시 변경 사항이 많을수록 장애가 발생할 수 있는 확률은 당연히 증가하기 때문에 개발자에게는 배포는 무서운 존재가 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 &lt;a href=&quot;https://about.gitlab.com/topics/version-control/what-is-gitlab-flow/&quot;&gt;GitLab-flow&lt;/a&gt;정도면 많은 사람이 협업하는 인터넷 서비스를 개발하는 브랜치 전략으로도 괜찮다고 생각합니다. 이를 참고해서 필요한 나름의 정책을 추가로 정의할 수도 있겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git-flow의 창시자 Vincent Driessen는 그의 아티클에 10년간의 회고를 덧붙였습니다. 거기서 지속적인 배포를 하는 프로젝트에서는 GitHub-flow와 같은 더 단순한 모델을 권장한다는 언급을 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://nvie.com/posts/a-successful-git-branching-model/&quot; class=&quot;bare&quot;&gt;https://nvie.com/posts/a-successful-git-branching-model/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If your team is doing continuous delivery of software, I would suggest to adopt a much simpler workflow (like GitHub flow) instead of trying to shoehorn git-flow into your team.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;귀사의 팀이 지속적으로 소프트웨어를 제공하고 있다면, 귀사의 팀에 git-flow를 도입하는 대신 훨씬 간단한 워크플로우(GitHub flow와 같은)를 채택하는 것을 제안합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(번역은 파파고에게 맡겼습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많은 조직에서는 특히 운영 환경에서는 지속적인 배포(continuous delivery)와는 먼 프로세스를 택하고 있을 수도 있습니다. 그럼에도 개발과 테스트 환경에서라도 자주 활발히 통합되어 테스트되고 리뷰되는걸 추구한다면 Vincent의 회고를 더 깊이 새겨둘만합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 문장으로 Vincent의 10년만의 회고는 끝이 납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;To conclude, always remember that panaceas don&amp;#8217;t exist. Consider your own context. Don&amp;#8217;t be hating. Decide for yourself.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;결론적으로, 만병통치약은 존재하지 않는다는 것을 항상 기억하세요. 자신만의 맥락을 고려하세요. 미워하지 말고, 스스로 결정하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Jackson으로 파싱한 JSON 속성값을 생성자로 전달하기</title>
      <link>https://blog.benelog.net//jackson-with-constructor.html</link>
      <pubDate>Sun, 15 Mar 2020 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">jackson-with-constructor.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#jackson에서_no_creators_like_default_construct_exist_에러_메시지&quot;&gt;1. Jackson에서 &lt;code&gt;(no Creators, like default construct, exist)&lt;/code&gt; 에러 메시지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#생성자로_json_속성값을_전달하는_방법들&quot;&gt;2. 생성자로 JSON 속성값을 전달하는 방법들&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#jsoncreator&quot;&gt;2.1. &lt;code&gt;@JsonCreator&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#constructorproperties&quot;&gt;2.2. &lt;code&gt;@ConstructorProperties&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#parameternamemodule_활용&quot;&gt;2.3. ParameterNameModule 활용&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#예제_소스_저장소&quot;&gt;3. 예제 소스 저장소&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;변경이력&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2023.06.25 : Spring Boot Gradle plugin에서 &lt;code&gt;-parameter&lt;/code&gt; 옵션을 넣어주는 소스로의 링크 변경&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2022.05.07 : 예제 프로젝트를 JDK17로 변경&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jackson에서_no_creators_like_default_construct_exist_에러_메시지&quot;&gt;1. Jackson에서 &lt;code&gt;(no Creators, like default construct, exist)&lt;/code&gt; 에러 메시지&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;파싱하고자하는 JSON&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;{
    &quot;accessDateTime&quot;: &quot;2019-10-10T11:14:16Z&quot;,
    &quot;ip&quot;: &quot;175.242.91.54&quot;,
    &quot;username&quot;: &quot;benelog&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위와 같은 JSON을 파생해서 아래와 같이 setter가 없는 객체에 집어 넣고 싶은 경우가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;파싱한 결과를 넣을 클래스&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class AccessLog {
    private final Instant accessDateTime;
    private final String ip;
    private final String username;

    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    public Instant getAccessDateTime() {
        return accessDateTime;
    }

    public String getIp() {
        return ip;
    }

    public String getUsername() {
        return username;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jackson 라이브러리로 JSON을 파싱하는 테스트 코드를 아래처럼 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;테스트 코드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;class ConstructorPropertiesTest {
    @Test
    void parse() throws JsonProcessingException {
        var json = &quot;&quot;&quot;
            {
            &quot;accessDateTime&quot;: &quot;2019-10-10T11:14:16Z&quot;,
            &quot;ip&quot;: &quot;175.242.91.54&quot;,
            &quot;username&quot;: &quot;benelog&quot;
            }
            &quot;&quot;&quot;;

        var objectMapper = new ObjectMapper()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .registerModule(new JavaTimeModule());

        AccessLog accessLog = objectMapper.readValue(json, AccessLog.class);

        then(accessLog.getAccessDateTime()).isEqualTo(&quot;2019-10-10T11:14:16Z&quot;);
        then(accessLog.getIp()).isEqualTo(&quot;175.242.91.54&quot;);
        then(accessLog.getUsername()).isEqualTo(&quot;benelog&quot;);;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 코드를 실행하면 다음의 Exception이 떨어집니다.
&lt;code&gt;(no Creators, like default construct, exist)&lt;/code&gt; 이 핵심적인 메시지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `net.benelog.jackson.ConstructorPropertiesTest$AccessLog` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)&quot;{
&quot;accessDateTime&quot;: &quot;2019-10-10T11:14:16Z&quot;,
&quot;ip&quot;: &quot;175.242.91.54&quot;,
&quot;username&quot;: &quot;benelog&quot;
}
&quot;; line: 2, column: 1]

	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1592)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1058)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JSON을 파싱한 결과를 전달할 적절한 생성자를 찾지 못했을 때 발생하는 에러입니다.
이 문제를 해결하는 방법을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;생성자로_json_속성값을_전달하는_방법들&quot;&gt;2. 생성자로 JSON 속성값을 전달하는 방법들&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jsoncreator&quot;&gt;2.1. &lt;code&gt;@JsonCreator&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jackson에서 제공하는 &lt;code&gt;@JsonCreator&lt;/code&gt;, &lt;code&gt;@JsonProperty&lt;/code&gt; 를 값을 전달할 생성자와 메서드 파라미터에 붙입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;AccessLog의 생성자에 &lt;code&gt;@JsonCreator&lt;/code&gt; 선언&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class AccessLog {

    // 멤버 변수 선언 생략

    @JsonCreator
    public AccessLog(
        @JsonProperty(&quot;accessDateTime&quot;) Instant accessDateTime,
        @JsonProperty(&quot;ip&quot;) String ip,
        @JsonProperty(&quot;username&quot;) String username) {

        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    // getter 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSON의 속성명과 객체의 멤버변수명이 다를 때도 자연스럽게 활용할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;생성자가 여러 개일 때 Jackson에서 사용할 생성자를 명시적으로 지정할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Jackson에 의존적인 방법입니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Jar파일로 배포하는 클래스 안에서 이 방법을 사용하려면 Jackson에 대한 의존성이 추가됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JSON 파싱 라이브러리를 교체한다면 전체 클래스를 수정해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;constructorproperties&quot;&gt;2.2. &lt;code&gt;@ConstructorProperties&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 1.6부터 제공되었던 &lt;code&gt;@java.beans.ConstructorProperties&lt;/code&gt; 은 생성자의 파라미터 이름을 지정하는 표준적인 방법입니다.
이를 활용하면 생성자의 파라미터 이름을 Reflection API를 통해서 알 수 있습니다.
Jackson은 2.7.0버전부터 &lt;code&gt;@ConstructorProperties&lt;/code&gt; 를 인지합니다. ( &lt;a href=&quot;https://github.com/fasterxml/jackson-databind/issues/905&quot; class=&quot;bare&quot;&gt;https://github.com/fasterxml/jackson-databind/issues/905&lt;/a&gt; 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;생성자에 &lt;code&gt;@ConstructorProperties&lt;/code&gt; 으로 파라미터의 이름을 지정하면, Jackson에서는 동일한 이름의 JSON 속성값을 생성자로 넘겨줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;AccessLog의 생성자에 `@ConstructorProperties`로 속성명 지정&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.beans.ConstructorProperties;

public class AccessLog {

    // 멤버 변수 선언 생략

    @ConstructorProperties({&quot;accessDateTime&quot;, &quot;ip&quot;, &quot;username&quot;})
    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }

    // getter 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Lombok을 활용한다면 이 과정을 더 편하게 할 수 있습니다.
&lt;code&gt;lombok.config&lt;/code&gt; 를 다음과 같은 선언을 하면 Lombok에서 만드는 생성자에서 &lt;code&gt;@ConstructorProperties&lt;/code&gt; 를 자동으로 넣어줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;lombok.config 설정&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;properties&quot;&gt;lombok.anyConstructor.addConstructorProperties=true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@Builder&lt;/code&gt;, &lt;code&gt;@AllArgsConstructor&lt;/code&gt; 와 같은 애너테이션을 클래스에 붙이면 Lombok에서는 자동으로 생성자를 만들어줍니다.
이를 통해 JSON 파싱한 값을 넣을 클래스를 더 단순하게 만들 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Lombok을 이용한 AccessLog 클래스 선언&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Builder
@Getter
@ToString
public class AccessLog {
    private final Instant accessDateTime;
    private final String ip;
    private final String username;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 Lombok v1.16.20 전까지는 디폴트로 &lt;code&gt;@ConstructorProperties&lt;/code&gt; 을 넣어줬었다고 합니다.
이 이후 버전부터는 디폴트가 아니므로 &lt;code&gt;lombok.config&lt;/code&gt; 에 명시적인 선언이 필요합니다.
( &lt;a href=&quot;https://multifrontgarden.tistory.com/222&quot; class=&quot;bare&quot;&gt;https://multifrontgarden.tistory.com/222&lt;/a&gt; 참조 )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@ConstructorProperties&lt;/code&gt; 를 직접 쓸 때의 장단점은 다음과 같다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@JsonCreator&lt;/code&gt; + &lt;code&gt;@JsonProperties&lt;/code&gt; 보다는 코딩량이 조금 적습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Jackson에 의존적이지 않습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSON을 파싱한 값이 들어가는 클래스를 jar 파일로 배포할 때 Jackson의 의존관계가 딸려들어가지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;같은 방식을 지원하는 다른 JSON 파싱 라이브러리로 교체할 때 코드 변경이 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSON의 속성명과 생성자의 실제 파라미터 명이 다른 경우에는 사용하는 것이 부자연스럽습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;만약 아래와 같이 &lt;code&gt;@ConstructorProperties&lt;/code&gt; 에서는 &quot;ip_address&quot;로 지정한 속성이 실제 파라미터이름이 &lt;code&gt;String ip&lt;/code&gt; 경우라면, 코드로는 잘 동작하지만 애너테이션의 원래 의도와는 어긋난 것이 아닌가 하는 생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @ConstructorProperties({&quot;accessDateTime&quot;, &quot;ip_address&quot;, &quot;username&quot;})
    public AccessLog(Instant accessDateTime, String ip, String username) {
        this.accessDateTime = accessDateTime;
        this.ip = ip;
        this.username = username;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@ConstructorProperties&lt;/code&gt; + Lombok 은 코드량이 적다는 장점이 있지만 멤버 변수의 이름이 JSON 속성명과 일치해야 한다는 단점도 있습니다.
jar 파일로 배포하는 클래스라면 Lombok에 대한 의존성이 부담스러울수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;parameternamemodule_활용&quot;&gt;2.3. ParameterNameModule 활용&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 예제들을 보면 &lt;code&gt;@JsonProperty(&quot;ip&quot;)&lt;/code&gt; 와 같이 지정하는 속성의 이름과 생성자의 파라미터의 이름이 동일합니다.
&lt;code&gt;String ip&lt;/code&gt; 와 같이 생성자의 파라미터의 이름을 바로 가지고 올 수 있다면 일일히 속성명을 지정하지 않을 수 있겠다는 생각이 들만합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 JDK 8이 나오기 전까지는 Reflection만으로는 파라미터 이름을 가지고 올 수 없었고, ASM과 같은 바이트코드 조작 라이브러리를 이용해서 디버깅을 위한 정보를 이용해야만 가능했습니다. ( &lt;a href=&quot;https://stackoverflow.com/questions/2729580/how-to-get-the-parameter-names-of-an-objects-constructors-reflection#2729907&quot; class=&quot;bare&quot;&gt;https://stackoverflow.com/questions/2729580/how-to-get-the-parameter-names-of-an-objects-constructors-reflection#2729907&lt;/a&gt; 참조) 그래서 앞서 소개한 &lt;code&gt;@java.beans.ConstructorProperties&lt;/code&gt; 와 같은 애너테이션도 활용되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK8 이상에서는 컴파일을 할 때 &lt;code&gt;-parameters&lt;/code&gt; 라는 옵션을 붙이면 Reflection API로 파라미터 정보를 가지고 올수 있도록 컴파일된 클래스에 정보를 덧붙여 줍니다.
Gradle을 쓰고 있다면 아래와 같이 설정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;build.gradle 안의 컴파일 옵션에 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;tasks.withType(JavaCompile).each {
    it.options.compilerArgs.add(&apos;-parameters&apos;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IDE 안에서도 컴파일 옵션을 신경써줘야합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IntelliJ에서는 &lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Build, Execution, Development&lt;/code&gt; &amp;gt; &lt;code&gt;Build Tools&lt;/code&gt; &amp;gt; &lt;code&gt;Gradle&lt;/code&gt; 에서 &lt;code&gt;Build and Run using:&lt;/code&gt; 옵션을 확인해 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jackson/intellij-settings-gradle.png&quot; alt=&quot;intellij-settings-gradle.png&quot; title=&quot;Settings의 Gradle 설정&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 옵션값이 &lt;code&gt;Gradle(Default)`로 되어 있다면, `build.gradle&lt;/code&gt; 의 컴파일 옵션이 그대로 쓰입니다.
만약 그 값이 &lt;code&gt;IntelliJ IDEA&lt;/code&gt; 로 되어 있다면 IntelliJ 안에서의 Java 컴파일 옵션도 동일하게 맞춰 줘야합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Build, Execution, Development&lt;/code&gt; &amp;gt; &lt;code&gt;Compiler&lt;/code&gt; &amp;gt; &lt;code&gt;Java Compiler&lt;/code&gt; 메뉴에서 &lt;code&gt;Addtional command line parameters&lt;/code&gt; 옵션에 &lt;code&gt;-parameters&lt;/code&gt; 을 적어줍니다.
옵션을 바꾼 후에는 전체 프로젝트를 리빌드합니다.
( &lt;code&gt;Build&lt;/code&gt; &amp;gt; &lt;code&gt;Rebuild Project&lt;/code&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jackson/intellij-settings-java-compiler.png&quot; alt=&quot;intellij-settings-java-compiler.png&quot; title=&quot;Settings의 Java Compiler 설정&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jackson의 ParameterNameModule 을 쓰기 위해서는 다음과 같이 의존성을 추가해야합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ParameterNameModule 의존성 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;    implementation &apos;com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.3&apos;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ObjectMapper&lt;/code&gt; 선언에서는 &lt;code&gt;registerModule()&lt;/code&gt; 메서드로 &lt;code&gt;ParameterNamesModule&lt;/code&gt; 을 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ObjectMapper에 ParameterNamesModule 추가&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;ObjectMapper 선언&quot;&gt;    var objectMapper = new ObjectMapper()
        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .registerModule(new JavaTimeModule())
        .registerModule(new ParameterNamesModule());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 하면 생성자에 특별한 애너테이션을 붙이지 않아도 Jackson은 JSON의 속성을 생성자에게 전달됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Boot에서는 &lt;code&gt;ParameterNamesModule&lt;/code&gt; 을 편하게 쓸 수 있도록 아래와 같은 기본 설정이 제공됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring Boot Gradle Plugin에서 Java 컴파일의 &lt;code&gt;-parameters&lt;/code&gt; 옵션이 자동 추가됩니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;( &lt;a href=&quot;https://github.com/spring-projects/spring-boot/blob/main/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java#L261&quot;&gt;JavaPluginAction.java#L90&lt;/a&gt; 참조 )&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;spring-boot-starter-web&lt;/code&gt; 에서 이미 &lt;code&gt;jackson-module-parameter-names&lt;/code&gt; 에 대한 의존성이 추가되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web/2.2.5.RELEASE&quot;&gt;spring-boot-starter-web&lt;/a&gt; &amp;#8594; &lt;a href=&quot;https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-json/2.2.5.RELEASE&quot;&gt;spring-boot-starter-json&lt;/a&gt; &amp;#8594; &lt;code&gt;jackson-module-parameter-names&lt;/code&gt; 로 의존관계가 연결됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;디폴트로 등록되는 &lt;code&gt;ObjectMapper&lt;/code&gt; bean에는 &lt;code&gt;ParameterNamesModule&lt;/code&gt; 이 이미 추가되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/blob/33e414fcb2f04bec653f799228907a577ac27a10/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java#L108&quot;&gt;JacksonAutoConfiguration.java#L108&lt;/a&gt; 참조&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RestTeamplteBuilder&lt;/code&gt; 로 &lt;code&gt;RestTemplate&lt;/code&gt; 을 생성한다면 디폴트 등록된 ObjectMapper 을 참조하는  &lt;code&gt;MappingJackson2HttpMessageConverter&lt;/code&gt; 가 &lt;code&gt;RestTemplate&lt;/code&gt; 에 주입됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ParameterNamesModule&lt;/code&gt; 은 Lombok에서 자동으로 만든 생성자도 잘 인식합니다.
&lt;code&gt;lombok.config&lt;/code&gt; 에 추가 설정을 하지 않아도 된다는 점이 &lt;code&gt;@ConstructorProperties&lt;/code&gt; 를 쓸 때와의 차이점입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 방식의 장단점은&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드가 짧습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Jackson에 대한 의존성이 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;생성자의 파라미터명과 JSON 속성의 이름이 반드시 일치해야 합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;생성자의 파라미터 이름이 JSON파싱에 쓰인다는것을 의식하지 않는다면, 파라미터 명을 잘 모르고 고쳐서 JSON 파싱이 안되게 하는 부작용이 쓰일수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;컴파일 옵션을 의식하지 않으면 특정 개발자의 IDE에서는 의도대로 동작하지 않을수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;생성자가 여러 개 일때는 &lt;code&gt;@JsonCreator&lt;/code&gt; 와 같은 다른 방식과 병행해서 써야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;예제_소스_저장소&quot;&gt;3. 예제 소스 저장소&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예제는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/jackson-experiment&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/blog/tree/main/examples/jackson-experiment&lt;/a&gt; 에 올려두었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@JsonCreator&lt;/code&gt; 활용 : &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/jackson-experiment/src/test/java/net/benelog/jackson/JsonCreatorTest.java&quot;&gt;JsonCreatorTest.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@ConstructorProperties&lt;/code&gt; 활용 : &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/jackson-experiment/src/test/java/net/benelog/jackson/ConstructorPropertiesTest.java&quot;&gt;ConstructorPropertiesTest.java&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;+ Lombok : &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/jackson-experiment/src/test/java/net/benelog/jackson/LombokTest.java&quot;&gt;LombokTest.java&lt;/a&gt;, &lt;a href=&quot;lombok.config&quot;&gt;lombok.config&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ParameterNameModule&lt;/code&gt; 활용 : &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/jackson-experiment/src/test/java/net/benelog/jackson/ParameterNameModuleTest.java&quot;&gt;ParameterNameModuleTest.java&lt;/a&gt;, &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/jackson-experiment/build.gradle#L28&quot;&gt;build.gradle&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 예제는 JDK 17을 써서 작성했습니다.
Text blocks 문법을 썼기 때문에 JDK 15이상이 필요합니다.
이 문법이 &apos;Preview&amp;#8217;로 들어간 JDK 13,14에서는 컴파일 옵션으로 &apos;--enable-preview&apos; 을 넣어야합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>ktlint로 Kotlin 공식 코딩 컨벤션 맞추기</title>
      <link>https://blog.benelog.net//ktlint.html</link>
      <pubDate>Tue, 12 Nov 2019 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">ktlint.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#gradle_빌드_설정&quot;&gt;1. Gradle 빌드 설정&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#editorconfig_설정&quot;&gt;1.1. &lt;code&gt;.editorconfig&lt;/code&gt; 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#스타일_검사&quot;&gt;1.2. 스타일 검사&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#스타일_일괄_변환&quot;&gt;1.3. 스타일 일괄 변환&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#git_hook으로_설정&quot;&gt;1.4. Git hook으로 설정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#intellij_설정&quot;&gt;2. IntelliJ 설정&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#포매터_설정&quot;&gt;2.1. 포매터 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#기존_코드_일괄_변환&quot;&gt;2.2. 기존 코드 일괄 변환&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#파일을_저장할_때마다_포매터_적용하기&quot;&gt;2.3. 파일을 저장할 때마다 포매터 적용하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#포매터가_바꿔주지_않는_규칙_신경쓰기&quot;&gt;2.4. 포매터가 바꿔주지 않는 규칙 신경쓰기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kotlin 언어의 공식 사이트에서는 코딩 컨벤션 가이드를 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://kotlinlang.org/docs/reference/coding-conventions.html&quot; class=&quot;bare&quot;&gt;https://kotlinlang.org/docs/reference/coding-conventions.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://ktlint.github.io/&quot;&gt;ktlint&lt;/a&gt;는 Kotlin의 공식 가이드의 규칙을 포함하여 코드 스타일을 검사하고, 맞춰주는 도구입니다.
이 글에는 ktlint를 Gradle, IntelliJ, Git과 연동하여 사용하는 방법을 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gradle_빌드_설정&quot;&gt;1. Gradle 빌드 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://ktlint.github.io/&quot;&gt;ktlint&lt;/a&gt;는 jar 파일로 제공되고, &lt;a href=&quot;https://jcenter.bintray.com/&quot; class=&quot;bare&quot;&gt;https://jcenter.bintray.com/&lt;/a&gt; 에도 배포되어 있습니다.
따라서 별도의 Gradle plugin이 없어도 Gradle에서 task를 정의해서도 쓸 수 있습니다.
그런데 ktlint의 공식 사이트에서는 Gradle plugin을 더 권장한다고 나와 있습니다.
이 글에서는 ktlint를 래핑한 Gradle plugin 중 Github에서 별 갯수가 가장 높은 &lt;a href=&quot;https://github.com/jlleitschuh/ktlint-gradle&quot;&gt;jlleitschuh/ktlint-gradle&lt;/a&gt;를 사용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;editorconfig_설정&quot;&gt;1.1. &lt;code&gt;.editorconfig&lt;/code&gt; 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ktlint는 &lt;code&gt;.editorconfig&lt;/code&gt; 파일에 선언된 규칙을 포함하여 코드 스타일을 검사합니다.
&lt;code&gt;.editorconfig&lt;/code&gt; 는 다양한 에디터와 IDE에서 공통적으로 지원하는 코드 스타일에 대한 설정 파일입니다.
자세한 스펙은 &lt;a href=&quot;https://editorconfig.org/&quot; class=&quot;bare&quot;&gt;https://editorconfig.org/&lt;/a&gt; 에서 파악할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 &lt;code&gt;.editorconfig&lt;/code&gt;  설정을 추가하면 Kotlin 코딩컨벤션과 함께 쓰기에 무난합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;.editorconfig 예시&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
tab_width = 4

[*.{kt,kts}]
disabled_rules=import-ordering
# java.* 패키지를 의존하는 경우  IntelliJ의 Orgarnize Import 기능으로는 알파벳 순서대로 import 구문을 정렬할 수 없다.
# 이는 ktlint의 import-ordering 규칙과 맞지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ktlint는 &lt;code&gt;.editorconfig&lt;/code&gt; 파일이 없어도 디폴트 규칙으로 실행할 수 있습니다.
그렇지만 아래와 같은 이유로 명시적으로 이 파일을 선언하는 것을 권장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;의도하지 않게 상위 디렉터리의 &lt;code&gt;.editorconfig&lt;/code&gt; 가 참조되는 경우를 막아줍니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.editorconfig&lt;/code&gt; 파일의 규약에 따르면, 이 파일을 지원하는 도구들은 &lt;code&gt;root = true&lt;/code&gt; 라는 선언이 된 &lt;code&gt;.editorconfig&lt;/code&gt; 파일을 찾기 전까지는 모든 상위 디렉터리를 탐색합니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_1&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_1&quot; title=&quot;View footnote.&quot;&gt;1&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ktlint에서도 이 규약을 준수합니다. 따라서 특정 컴퓨터에만 소스를 복제한 디렉터리의 상위 디렉터리에 &lt;code&gt;.editorconfig&lt;/code&gt; 가 있다면 그 장비에서는 스타일 체크 결과가 다르게 나오게 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ktlint가 버전이 올라가면서 규칙의 디폴트 값이 변경될 경우를 대비합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;예를 들면 ktlint 0.34.0 버전부터 &lt;code&gt;insert_final_newline = true&lt;/code&gt; 가 디폴트 값이 되었습니다.  &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_2&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_2&quot; title=&quot;View footnote.&quot;&gt;2&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;다양한 IDE와 Editor등의 도구에서 이 설정을 참조합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ, VSCode, GitHub등에서 뷰어,포매터의 설정으로 자동 반영됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드 포맷에 대한 문서와 역할도 할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;위의 예시와 같이 &lt;code&gt;.editorconfig&lt;/code&gt; 에 선언되는 속성의 이름은 이해하기 쉽습니다. 이 파일만 봐도 파일 포맷에 대한 일부 규칙을 파악할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://kotlinlang.org/docs/reference/coding-conventions.html&quot;&gt;Kotlin 공식 코딩 컨벤션&lt;/a&gt;에 명시되지 않은 규칙까지 일치시킬 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;max_line_length&lt;/code&gt; , &lt;code&gt;insert_final_newline&lt;/code&gt; 과 같은 규칙을 Kotlin 공식 코딩 컨벤션에는 명시되어 있지는 않습니다. 그러나 같은 소스를 고치면서 협업하는 개발자들이 이를 통일하지 않을 경우 불필요한 diff 가 발생하여 코드 변경분을 파악할 때 불편해집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;disabled_rules&lt;/code&gt; 속성으로 검사하지 않을 규칙을 지정할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;특히 현시점에서는 &lt;code&gt;import-ordering&lt;/code&gt; 규칙은 쓰지 않는 것이 좋습니다. 위의 예시 파일에도 이를 반영했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jlleitschuh/ktlint-gradle&quot;&gt;jlleitschuh/ktlint-gradle&lt;/a&gt;는 아래와 같이 설정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Gradle Groovy설정 (build.gradle)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;groovy&quot;&gt;buildscript {
    repositories {
        maven {
            url &apos;https://plugins.gradle.org/m2/&apos;
        }
    }
    dependencies {
        classpath &apos;org.jlleitschuh.gradle:ktlint-gradle:9.1.0&apos;
    }
}

plugins {
    id &apos;org.jlleitschuh.gradle.ktlint&apos; version &apos;9.1.0&apos;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Gradle Kotlin설정 (build.gradle.kts)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;kotlin&quot;&gt;buildscript {
    repositories {
        maven(url = &quot;https://plugins.gradle.org/m2/&quot;)
    }
    dependencies {
        classpath(&quot;org.jlleitschuh.gradle:ktlint-gradle:9.1.0&quot;)
    }
}
plugins {
    id(&quot;org.jlleitschuh.gradle.ktlint&quot;) version &quot;9.1.0&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;admonitionblock caution&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Caution&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jlleitschuh/ktlint-gradle&quot;&gt;jlleitschuh/ktlint-gradle&lt;/a&gt;는 Gradle 버전 5.4.1이상에서만 사용할 수 있습니다. 그 이하 버전에서는 아래와 같은 에러가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;* What went wrong:
Could not determine the dependencies of task &apos;:ktlintCheck&apos;.
&amp;gt; Could not create task &apos;:ktlintTestSourceSetCheck&apos;.
   &amp;gt; Could not create task of type &apos;KtlintCheckTask&apos;.
      &amp;gt; Could not generate a decorated class for class org.jlleitschuh.gradle.ktlint.KtlintCheckTask.
         &amp;gt; org/gradle/work/InputChanges&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이럴 경우에는 Gradle의 버전을 업그레이드해야합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gradle Wrapper를 쓰고 있다면
&lt;code&gt;{프로젝트홈}/gradle/wrapper/gradle-wrapper.properties&lt;/code&gt; 파일의 distributionUrl 속성에 지정된 버전을 고쳐서 버전을 올릴 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다양한 Gradle 버전을 별도로 설치하는데에는 &lt;a href=&quot;https://sdkman.io/&quot;&gt;SDKMAN&lt;/a&gt; 을 권장합니다.
Gradle이 별도로 설치되어 있을 경우 Gradle Wrapper를 업그레이드하는 방법은 &lt;a href=&quot;https://java.ihoney.pe.kr/476&quot; class=&quot;bare&quot;&gt;https://java.ihoney.pe.kr/476&lt;/a&gt; 을 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gradle 버전을 업그레이드하기가 어려울 경우 &lt;a href=&quot;https://ktlint.github.io/&quot; class=&quot;bare&quot;&gt;https://ktlint.github.io/&lt;/a&gt; 에 안내된 다른 Gradle Plugin이나 Plugin없이 사용하는 방법을 참고하시기 바랍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gradle.properites&lt;/code&gt; 파일에도 공식 코딩 컨벤션을 사용한다는 정책을 아래와 같이 명시합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;gradle.properites 설정&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;properties&quot;&gt;kotlin.code.style=official&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IntellJ에서 프로젝트를 import할때 위의 설정을 참조할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;플러그인 설정이 완료되면 ktlint를 실행하는 Gradle 태스크는  &lt;code&gt;./gradlew tasks | grep ktlint&lt;/code&gt; 명령으로 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;스타일_검사&quot;&gt;1.2. 스타일 검사&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew ktlintCheck&lt;/code&gt; 태스크는 스타일 검사를 수행합니다.
이 태스크는 &lt;code&gt;./gradlew build&lt;/code&gt; 를 실행했을 때 연결되는 전체 프로젝트 빌드 싸이클에 포함됩니다. 따라서 디폴트 설정으로는  ktlint에서 가이드하는 코드 스타일을 지키지 않으면 빌드가 실패합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;스타일_일괄_변환&quot;&gt;1.3. 스타일 일괄 변환&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew ktlintFormat&lt;/code&gt; 태스크로는 스타일에 맞지 않는 코드를 바꿔줍니다. 전체 프로젝트의 소스 코드를 한꺼번에 고칠 때 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, 이 기능을 사용하다가 의도하지 않게 파일이 삭제되는 경우도 있었습니다.
그래서 이 태스크는 조심해서 사용해야합니다.
이 글에서 설명한 IntelliJ로 포맷을 일괄변환하는 방법도 참고해서 병행해서 사용하는 편이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;git_hook으로_설정&quot;&gt;1.4. Git hook으로 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 설명한 &lt;code&gt;ktlintCheck&lt;/code&gt; , &lt;code&gt;ktlintFormat&lt;/code&gt; 태스크를 Git의 Hook으로 등록하여 commit을 하면 자동으로 실행되게 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;./gradlew addKtlintCheckGitPreCommitHook&lt;/code&gt; : ktlintCheck 태스크를 pre-commit hook으로 등록&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;./gradlew addKtlintFormatGitPreCommitHook&lt;/code&gt; : ktlintFormat 태스크를 pre-commit hook으로 등록ktlintCheck 태스크로 검사하도록 설정합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 설명한 것처럼 &lt;code&gt;ktlintFormat&lt;/code&gt; 태스크는 의도하지 않게 파일을 바꿀 위험이 있기 때문에 &lt;code&gt;addKtlintCheckGitPreCommitHook&lt;/code&gt; 를 더 권장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;등록된 pre-commit hook은 &lt;code&gt;rm .git/hooks/pre-commit&lt;/code&gt; 명령으로 삭제할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;intellij_설정&quot;&gt;2. IntelliJ 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IntelliJ의 코드 포매터는 &lt;a href=&quot;https://kotlinlang.org/docs/reference/coding-conventions.html&quot;&gt;Kotlin 공식 코딩 컨벤션&lt;/a&gt;이 나오기 전부터 쓰이던 디폴트 설정이 있었습니다. IntelliJ에서는 Kotlin 1.3에서는 신규로 생성되는 프로젝트에서, Kotlin 1.4에서는 모든 프로젝트에서 공식 컨벤션에 맞춘 포매터가 디폴트로 설정될 것이라고 합니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_3&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_3&quot; title=&quot;View footnote.&quot;&gt;3&lt;/a&gt;]&lt;/sup&gt; 오랫동안 유지보수할 Kotlin 코드라면 공식 컨벤션에 맞춰서 IntelliJ에서도 포매터를 설정되었는지 신경을 써야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;포매터_설정&quot;&gt;2.1. 포매터 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음에 안내한 A, B 2가지 방법 중의 하나를 선택하셔서  공식 코딩컨벤션에 맞게 IntelliJ의 포매터 설정을 맞출수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;a_predefined_을_이용한_설정&quot;&gt;2.1.1. A. `predefined ` 을 이용한 설정&lt;/h4&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ 메뉴에서 &lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Editor&lt;/code&gt; &amp;gt; &lt;code&gt;Code Style&lt;/code&gt; &amp;gt; &lt;code&gt;Kotlin&lt;/code&gt; 으로 이동합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Scheme&lt;/code&gt; 항목을 Project로 지정합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;여러 프로젝트에서 다른 설정을 쓸 경우를 대비해 가급적 글로벌 설정을 바꾸지 않기 위함입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Set from&amp;#8230;&amp;#8203;&lt;/code&gt; &amp;#8594; &lt;code&gt;Predefined Style&lt;/code&gt; &amp;#8594; &lt;code&gt;Kotlin Style Guide&lt;/code&gt; 를 선택합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/kotlin-code-style/intelli-j-kotlin-style-guide.gif&quot; alt=&quot;IntelliJ Kotlin Style&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;b_gradle_plugin을_이용한_설정&quot;&gt;2.1.2. B. Gradle plugin을 이용한 설정&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞서 설정한 &lt;a href=&quot;https://github.com/jlleitschuh/ktlint-gradle&quot;&gt;jlleitschuh/ktlint-gradle&lt;/a&gt;를 이용하여 &lt;code&gt;./gradlew ktlintApplyToIdea&lt;/code&gt; 태스크를 실행합니다.
이 태스크는 IntelliJ 설정파일의 코드 스타일 부분을 덮어써서 ktlint의 규칙과 가급적 맞는 포매터가 설정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;포매터가 설정되면 파일을 편집할 때 &lt;code&gt;Code&lt;/code&gt; &amp;gt; &lt;code&gt;Reformat Code&lt;/code&gt; 메뉴를 선택하거나 단축키 &lt;code&gt;Ctrl + Shift + L&lt;/code&gt; 단축키로 포매터를 적용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;기존_코드_일괄_변환&quot;&gt;2.2. 기존 코드 일괄 변환&lt;/h3&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ의 프로젝트 탐색기에서 프로젝트의 최상위 디렉터리를 우클릭합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reformat Code 를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;우클릭을 하여 나오는 메뉴에서 &lt;code&gt;Reformat Code&lt;/code&gt; 를 선택하거나&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + Shift + L&lt;/code&gt; 단축키를 누릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;파일을_저장할_때마다_포매터_적용하기&quot;&gt;2.3. 파일을 저장할 때마다 포매터 적용하기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/7642-save-actions&quot;&gt;Save Actions Plugin&lt;/a&gt;를 사용하면 파일을 저장할 때 자동으로 포매터를 실행할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;File&lt;/code&gt; &amp;gt; &lt;code&gt;Settings&lt;/code&gt; ( &lt;code&gt;Ctrl + Alt + S&lt;/code&gt; ) &amp;gt; &lt;code&gt;Plugins&lt;/code&gt; 메뉴로 이동합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Marketplace&lt;/code&gt; 탭에서 &apos;Save Actions&apos; 로 검색합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Save Actions&apos; plugin의 상세 설명 화면에서  `[Install]&lt;/code&gt; 버튼을 누릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ를 재시작합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;File&lt;/code&gt; &amp;gt; &lt;code&gt;Settings&lt;/code&gt; &amp;gt;  &lt;code&gt;Other Settions&lt;/code&gt; &amp;gt; &lt;code&gt;Save Actions&lt;/code&gt; 메뉴로 이동합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;아래 항목 혹은 그외의 원하는 정책을 체크합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Activate save actions on save&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Optimize imoprts&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Refomat file&lt;/code&gt; (전체 프로젝트의 스타일이 통일된 경우)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Refomat only changed code&lt;/code&gt; (프로젝트의 스타일이 통일되어 있지 않아서 스타일이 맞지 않는 코드를 함께 고치면 변경 부분을 알아보기가 더 어려운 경우)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/kotlin-code-style/intelli-j-save-actions-config.jpg&quot; alt=&quot;IntelliJ Save Actions&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;포매터가_바꿔주지_않는_규칙_신경쓰기&quot;&gt;2.4. 포매터가 바꿔주지 않는 규칙 신경쓰기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IntelliJ의 포매터는 ktlint에서 검사하는 규칙을 다 자동을 맞추어주지는 못합니다.
즉 &lt;code&gt;Reformat Code&lt;/code&gt; ( &lt;code&gt;Ctrl + Alt + L&lt;/code&gt; ) 을 실행하는 것만으로는 ktlint 검사를 통과한다는 보장은 없습니다.
다음에서 설명하는 IntelliJ의 기능을 잘 활용하면 보다 빠른 시점에서 ktlint의 규칙을 준수하는데 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;파일의_마지막에_자동으로_개행문자_추가&quot;&gt;2.4.1. 파일의 마지막에 자동으로 개행문자 추가&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;POSIX 명세에 따라서, 텍스트 파일의 마지막에 개행문자(LF)를 추가하는 것이 권장됩니다. &lt;sup class=&quot;footnote&quot;&gt;[&lt;a id=&quot;_footnoteref_4&quot; class=&quot;footnote&quot; href=&quot;#_footnotedef_4&quot; title=&quot;View footnote.&quot;&gt;4&lt;/a&gt;]&lt;/sup&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, IntelliJ의 &lt;code&gt;Reformat Code&lt;/code&gt; 기능으로는 마지막 개행문자 추가가 되지 않습니다.
&lt;code&gt;File&lt;/code&gt; &amp;gt; &lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Editor&lt;/code&gt; &amp;gt; &lt;code&gt;General&lt;/code&gt; 메뉴에서 &lt;code&gt;Ensure line feed at file end on Save&lt;/code&gt; 를 선택하면, 파일이 저장될 때 자동으로 마지막에 개행문자를 추가해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/kotlin-code-style/intelli-j-lf-eof.jpg&quot; alt=&quot;IntelliJ line feed end of file&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 설정은 &lt;code&gt;.editorconfig&lt;/code&gt; 의 선언에 따라 자동으로 활성화될 수도 있습니다.
그렇지만 의도대로 동작하지 않는다면 한번 확인해볼만합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;intellij_경고에_따라_고치기&quot;&gt;2.4.2. IntelliJ 경고에 따라 고치기&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kotlin 공식 코딩 컨벤션에는 공백 등 단순한 파일 형식 외에도 문법적인 요소에 대한 것도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들면 String Template를 쓸 때는 꼭 필요한 경우가 아니면 중괄호를 넣지 말라는 규칙이 있습니다.
( &lt;a href=&quot;https://kotlinlang.org/docs/reference/coding-conventions.html#string-templates&quot; class=&quot;bare&quot;&gt;https://kotlinlang.org/docs/reference/coding-conventions.html#string-templates&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(O): &lt;code&gt;println(&quot;$name is my friend.&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(X): &lt;code&gt;println(&quot;${name} is my friend.&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 규칙을 어긴 코드는 &apos;Reformat Code&apos; 로는 바로 바뀌지 않습니다.
IntelliJ에서 이런 코드에 경고를 보내고 &lt;code&gt;Alt + Shift + Enter&lt;/code&gt; 단축키로 코드로 바꾸는 기능을 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/kotlin-code-style/intelli-j-string-template-warning.jpg&quot; alt=&quot;IntelliJ String template&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이처럼 IntelliJ에서는 언어 문법을 활용할때도 Kotlin 공식 코딩 컨벤션에서 권장하는 스타일대로 쓰도록 유도하고 있습니다.
이런 경고들을 무시하지 않고 반영한다면 ktlint의 검사에서도 통과할 가능성이 높아집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;footnotes&quot;&gt;
&lt;hr&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_1&quot;&gt;
&lt;a href=&quot;#_footnoteref_1&quot;&gt;1&lt;/a&gt;. &lt;a href=&quot;https://editorconfig.org/&quot; class=&quot;bare&quot;&gt;https://editorconfig.org/&lt;/a&gt; 에서 &apos;When opening a file, EditorConfig plugins look for a file named .editorconfig in the directory of the opened file and in every parent directory.&apos;
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_2&quot;&gt;
&lt;a href=&quot;#_footnoteref_2&quot;&gt;2&lt;/a&gt;. 0.34.0 릴리즈 노트 참고 : &lt;a href=&quot;https://github.com/pinterest/ktlint/blob/master/CHANGELOG.md#0340---2019-07-15&quot; class=&quot;bare&quot;&gt;https://github.com/pinterest/ktlint/blob/master/CHANGELOG.md#0340---2019-07-15&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_3&quot;&gt;
&lt;a href=&quot;#_footnoteref_3&quot;&gt;3&lt;/a&gt;. &lt;a href=&quot;https://kotlinlang.org/docs/reference/code-style-migration-guide.html&quot; class=&quot;bare&quot;&gt;https://kotlinlang.org/docs/reference/code-style-migration-guide.html&lt;/a&gt; 에서 설명된 계획입니다.
&lt;/div&gt;
&lt;div class=&quot;footnote&quot; id=&quot;_footnotedef_4&quot;&gt;
&lt;a href=&quot;#_footnoteref_4&quot;&gt;4&lt;/a&gt;. &lt;a href=&quot;https://blog.coderifleman.com/2015/04/04/text-files-end-with-a-newline/&quot; class=&quot;bare&quot;&gt;https://blog.coderifleman.com/2015/04/04/text-files-end-with-a-newline/&lt;/a&gt; 참조
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Jürgenized: Spring 리드 개발자의 이름에서 나온 코드 품질 용어</title>
      <link>https://blog.benelog.net//juergenized.html</link>
      <pubDate>Thu, 4 Jul 2019 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">juergenized.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#jürgenization의_정의&quot;&gt;Jürgenization의 정의&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#spring_project_infrastructure_발표와_refactor_type_분류&quot;&gt;Spring Project Infrastructure 발표와 Refactor Type 분류&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#머지_대신_직접_커밋_pr_23200의_처리_과정&quot;&gt;머지 대신 직접 커밋: PR #23200의 처리 과정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rod_johnson이_꼽은_가장_생산적인_프로그래머&quot;&gt;Rod Johnson이 꼽은 가장 생산적인 프로그래머&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;Jürgenized&amp;#8217;라는 용어의 유래와 실제 사례를 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jürgenization의_정의&quot;&gt;Jürgenization의 정의&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Data 프로젝트를 이끌었던 Oliver Drotbohm은 2013년에 &lt;a href=&quot;https://odrotbohm.de/2013/03/juergenized/&quot;&gt;Jürgenized&lt;/a&gt;라는 글에서 이 용어를 사전 항목 형식으로 정의했습니다.  번역하면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jürgenization (명사)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;당장의 문제를 푸는 데는 충분해 보이는 코드를, 견고하고 품질이 보증되며 완벽하게 문서화되고 확장 가능한 코드로 바꾸는 과정. 이 과정은 원래 코드의 전면 재작성(Karma level 0)부터 사소한 수정(Karma level 10)까지 다양한데, 보통 코드 작성자가 이 과정을 얼마나 자주 통과해 봤는지에 따라 달라진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글은 용어의 유래도 밝힙니다. Spring Framework에 들어올 기여 코드를 다루는 Juergen Hoeller의 방식에서 나온 말로, 코드를 core codebase에 넣으려면 코드와 그 작성자가 함께 jürgenized 되어야 한다는 것입니다. 글이 쓰인 시점에는 Spring 생태계 프로젝트 전반의 코드 품질 보증 과정을 가리키는 말로 의미가 넓어져 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spring_project_infrastructure_발표와_refactor_type_분류&quot;&gt;Spring Project Infrastructure 발표와 Refactor Type 분류&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 이 단어를 SpringSource의 Gunnar Hillert와 Roy Clarkson이 2013년에 발표한 &lt;a href=&quot;https://www.slideshare.net/royclarkson/spring-project-infrastructure&quot;&gt;Spring Project Infrastructure&lt;/a&gt;라는 슬라이드에서 처음 봤습니다. Spring 프로젝트들이 GitHub로 이전하고 PR로 기여를 받는 과정을 소개하는 발표인데, 후반부에 &apos;Jürgenization&amp;#8217;이라는 슬라이드가 등장합니다. 같은 제목의 &lt;a href=&quot;https://www.youtube.com/watch?v=m74QsK5XCeQ&quot;&gt;발표 영상&lt;/a&gt;에서는 이를 Juergen Hoeller의 코딩 스타일과 기준에 맞는 코드 품질과 엄격함이라고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 용어를 리팩터링의 한 유형으로 분류한 문서도 있습니다. &lt;a href=&quot;https://web.archive.org/web/20210516210106/http://notes.experimental-software.com/SoftwareEngineering/Programming/Refactoring.html&quot;&gt;Refactoring 정리 노트&lt;/a&gt;는 Refactor Type을 TDD, Boy Scout Rule, Comprehension, Preparatory, Planned, Long term으로 나열하는데, 그 목록의 마지막 항목이 Jürgenizing입니다. 출처로는 앞서 소개한 Oliver Drotbohm의 글을 링크하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;머지_대신_직접_커밋_pr_23200의_처리_과정&quot;&gt;머지 대신 직접 커밋: PR #23200의 처리 과정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;spring-framework 저장소의 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/pulls&quot;&gt;PR 목록&lt;/a&gt;을 보면 Stéphane Nicoll 같은 커미터는 올라온 PR을 그대로 merge해 주는 경우가 많습니다. 반면 Juergen Hoeller는 PR을 리뷰하고 참고하되, 반영은 본인의 커밋으로 직접 하는 방식을 선호하는 듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 PR #23200도 그렇게 처리됐습니다. 흐름을 따라가 보면 Jürgenization의 축소판을 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;제안자가 bean destroy 실패 로그를 &lt;code&gt;info&lt;/code&gt;에서 더 높은 레벨로 올려달라는 PR을 올렸습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Juergen Hoeller가 리뷰하면서 새로운 관점을 제시했습니다. 이 로그는 Spring 5.0/4.3에서는 &lt;code&gt;warn&lt;/code&gt; 레벨이었는데 5.1에서 로그 레벨 정책을 정비하다가 생긴 회귀라는 점을 짚었고, 제안대로 &lt;code&gt;error&lt;/code&gt;까지 올리는 방안에는 bean destroy 실패가 여러 단계로 이뤄지는 소멸 과정의 부분적인 실패일 뿐이므로 불필요한 노이즈를 만든다며 선을 그었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;결론은 PR의 merge가 아니었습니다. Juergen Hoeller가 직접 작성한 &lt;a href=&quot;https://github.com/spring-projects/spring-framework/commit/56cc0d02e9&quot;&gt;커밋&lt;/a&gt;이 &quot;Bean destruction exceptions consistently logged at warn level&quot;이라는 메시지와 함께 반영됐고, PR은 이 커밋의 &quot;Closes gh-23200&quot;으로 닫혔습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제안한 사람의 문제의식은 받아들이되, 원인 분석과 최종 코드는 자신의 기준으로 다시 만들어내는 과정이 그대로 드러납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;rod_johnson이_꼽은_가장_생산적인_프로그래머&quot;&gt;Rod Johnson이 꼽은 가장 생산적인 프로그래머&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Juergen Hoeller의 이런 위상을 보여주는 인터뷰가 있습니다. Ed Burns가 쓴 &lt;a href=&quot;https://www.goodreads.com/book/show/227849.Secrets_of_the_Rock_Star_Programmers&quot;&gt;Secrets of the Rock Star Programmers&lt;/a&gt;(2008, 번역서 제목은 &apos;세상을 뒤흔든 프로그래머들의 비밀&apos;)에 실린 Rod Johnson 인터뷰입니다. 제 기억에 따르면, 함께 일했던 프로그래머 가운데 가장 생산적이었던 사람이 누구냐는 질문에 Rod Johnson은 생각할 것도 없이 Juergen Hoeller라고 답합니다. 이어서 Juergen Hoeller가 개발 속도와 품질 모든 면에서 가장 뛰어난 결과를 보이며, 자신이라면 여러 번의 시행착오를 거쳤을 문제를 그는 한 번에 해결할 것이라고 말합니다. (번역판 p32~33 쪽)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://image.yes24.com/momo/TopCate80/MidCate07/7963859.jpg&quot; alt=&quot;Book cover&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이미지 출처 : &lt;a href=&quot;https://www.yes24.com/product/goods/3691866&quot; class=&quot;bare&quot;&gt;https://www.yes24.com/product/goods/3691866&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://covers.openlibrary.org/b/isbn/9780071490832-L.jpg&quot; alt=&quot;Secrets of the Rock Star Programmers book cover&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이미지 출처 : &lt;a href=&quot;https://openlibrary.org/isbn/9780071490832&quot; class=&quot;bare&quot;&gt;https://openlibrary.org/isbn/9780071490832&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원서에서 해당 부분은 15쪽에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ed: Who is the most productive programmer you&amp;#8217;ve ever worked with? What was it about them that made them that way? Did they just crank out lots of code? Did they get things done faster than everyone else? What do you say about that?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rod: Jürgen Höller. Undoubtedly.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ed: And what is it about Jürgen?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rod: It&amp;#8217;s basically all of the above, which is kind of demoralizing. Maybe the difference isn&amp;#8217;t that big between Jürgen and other good people, but I&amp;#8217;ve worked with some people that I would rate as better overall, but I&amp;#8217;ve never worked with or encountered anyone who&amp;#8217;s better in every way. For example, if I look at the other people in the Spring team, some are faster [than others], but do higher-quality code. Others do higher-quality code, but are slower. Some of them, other than Jürgen, are probably better overall, but, nevertheless, you can break it down and see differences and weaknesses. Whereas, Jürgen, compared to anyone I&amp;#8217;ve ever seen, is faster and his code is better. You asked how does he do it? I don&amp;#8217;t know. From what I&amp;#8217;ve seen, it&amp;#8217;s not just my opinion. Jürgen&amp;#8217;s pretty legendary. What I&amp;#8217;ve seen is that he works in a very different way from me or anyone else that I know. For example, my way of working is very iterated. If it&amp;#8217;s a really complex problem, my first version would be embarrassing to look back on, my second version wouldn&amp;#8217;t be too bad, and the third version would be quite good. Jürgen would start with a problem like that, and this would take maybe a lapse time of ten hours&apos; programming if it was a reasonably hard problem. Jürgen would start off by thinking deeply about it for three hours. Then he&amp;#8217;d spend two hours writing code, and it&amp;#8217;d probably be perfect. I can&amp;#8217;t work like that.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;20년 가까이 지난 인터뷰인데, Juergen Hoeller는 지금도 Spring Framework의 리드로 활동하고 있습니다. 2024년에는 Spring Framework 1.0 릴리스 20주년을 맞아 Rod Johnson과 함께 &lt;a href=&quot;https://spring.io/blog/2024/04/18/a-bootiful-podcast-spring-founders-rod-johnson-and-juergen-hoeller-on-the/&quot;&gt;팟캐스트&lt;/a&gt;에 출연했고, &lt;a href=&quot;https://www.youtube.com/watch?v=i_fiHmg1qM0&quot;&gt;Spring I/O 2025의 발표 무대&lt;/a&gt;에도 함께 섰습니다. 한 프레임워크를 20년 넘게 같은 기준으로 지켜온 셈이니, &apos;Jürgenized&amp;#8217;라는 용어도 당분간 유효할 듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://odrotbohm.de/2013/03/juergenized/&quot;&gt;Oliver Drotbohm - Jürgenized&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.slideshare.net/royclarkson/spring-project-infrastructure&quot;&gt;Spring Project Infrastructure 슬라이드&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=m74QsK5XCeQ&quot;&gt;Spring Project Infrastructure 발표 영상&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/20210516210106/http://notes.experimental-software.com/SoftwareEngineering/Programming/Refactoring.html&quot;&gt;Refactoring - notes.experimental-software.com (Wayback Machine)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/23199&quot;&gt;spring-framework issue #23199&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/pull/23200&quot;&gt;spring-framework PR #23200&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/commit/56cc0d02e9&quot;&gt;Juergen Hoeller의 반영 커밋&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/227849.Secrets_of_the_Rock_Star_Programmers&quot;&gt;Secrets of the Rock Star Programmers&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.facebook.com/groups/springkorea/permalink/2056700451108240/&quot;&gt;한국 스프링 사용자 모임의 원글과 댓글&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>여러 개의 JDK를 설치하고 선택해서 사용하기</title>
      <link>https://blog.benelog.net//installing-jdk.html</link>
      <pubDate>Mon, 1 Jul 2019 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">installing-jdk.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#특별한_도구를_안_쓸_때의_jdk_설치_버전_선택&quot;&gt;1. 특별한 도구를 안 쓸 때의 JDK 설치 &amp;amp; 버전 선택&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_설치&quot;&gt;1.1. JDK 설치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_버전_선택&quot;&gt;1.2. JDK 버전 선택&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#다양한_jdk_설치와_사용을_편하게_하는_도구&quot;&gt;2. 다양한 JDK 설치와 사용을 편하게 하는 도구&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#aptyum&quot;&gt;2.1. APT/YUM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#update_alternatives_alternatives&quot;&gt;2.2. update-alternatives / alternatives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#homebrew&quot;&gt;2.3. Homebrew&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#chocolatey&quot;&gt;2.4. Chocolatey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#direnv&quot;&gt;2.5. direnv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jenv&quot;&gt;2.6. jEnv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sdkman&quot;&gt;2.7. SDKMAN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jabba&quot;&gt;2.8. jabba&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#무엇을_어떻게_사용할_것인가&quot;&gt;3. 무엇을 어떻게 사용할 것인가?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다양한 배포판과 버전의 JDK를 명령어 한 줄로 설치하고 OS의 셸에서 사용할 JDK를 쉽게 지정할 수 있게 해주는 도구들을 소개합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;주요 변경이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2023.03.27&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Jabba 설치를 안내하는 URL을 현재 관리되는 포크된 버전으로 변경&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2019.07.03&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Homebrew에 대한 소개를 별도의 단락으로 분리&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Chocolatey에 대한 설명 보강&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;특별한_도구를_안_쓸_때의_jdk_설치_버전_선택&quot;&gt;1. 특별한 도구를 안 쓸 때의 JDK 설치 &amp;amp; 버전 선택&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jdk_설치&quot;&gt;1.1. JDK 설치&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK를 수동으로 설치하는 절차는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;설치할 버전/배포판을 다운로드합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;예) Oracle의 OpenJDK 빌드 : &lt;a href=&quot;https://jdk.java.net/&quot; class=&quot;bare&quot;&gt;https://jdk.java.net/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예) AdoptOpenJDK : &lt;a href=&quot;https://adoptopenjdk.net/&quot; class=&quot;bare&quot;&gt;https://adoptopenjdk.net/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예) Graal VM Community Edition : &lt;a href=&quot;https://github.com/oracle/graal/releases&quot; class=&quot;bare&quot;&gt;https://github.com/oracle/graal/releases&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;다운로드한 파일의 압축을 풉니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OS의 환경변수를 지정합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;JAVA_HOME&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK의 압축을 푼 디렉터리를 지정합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Maven이나 Tomcat 같은 솔루션에서 이 환경변수로 JDK의 위치를 참조합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;PATH&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;쓰고 있던 &lt;code&gt;PATH&lt;/code&gt; 변수에 &lt;code&gt;$JAVA_HOME/bin&lt;/code&gt; 을 더합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;java&lt;/code&gt; , &lt;code&gt;javac&lt;/code&gt; 등을 명령행에서 직접 실행할 수 있도록 하기 위해서 하는 작업입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jdk_버전_선택&quot;&gt;1.2. JDK 버전 선택&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IDE에서는 프로젝트별로 사용할 JDK의 위치를 선택할 수 있습니다.
IntelliJ에서는 &lt;code&gt;File&lt;/code&gt; &amp;gt; &lt;code&gt;Project Structure&lt;/code&gt; &amp;gt; &lt;code&gt;Platform Settings&lt;/code&gt; (단축키 &lt;code&gt;Ctrl + Alt + Shift + S&lt;/code&gt; ) &amp;gt; &lt;code&gt;SDK&lt;/code&gt; 메뉴 에서 이를 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;:&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jdk/intelli-j-jdk.jpg&quot; alt=&quot;intelli-j-jdk.jpg&quot; title=&quot;IntelliJ 에서 JDK 선택&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OS의 명령행에서 Maven, Gradle로 직접 빌드를 하거나 &lt;code&gt;java -jar&lt;/code&gt; 로 직접 프로그램을 실행시킬 때를 대비해서 &lt;code&gt;JAVA_HOME&lt;/code&gt;, &lt;code&gt;PATH&lt;/code&gt;  설정이 되어 있어야합니다.
프로젝트마다 사용하는 JDK 버전이 다르면 사용할 JDK를 지정하기가 번거롭습니다.
매번 이런 &lt;code&gt;JAVA_HOME&lt;/code&gt; 같은 환경 변수를 바꾸거나 &lt;code&gt;/usr/lib/jvm/java-13-openjdk-amd64/bin/java&lt;/code&gt; 와 같이 전체 경로로 실행할 도구를 지정한다면 더욱 그렇습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반복적인 작업을 셸스크립트나 배치파일로 할 수도 있습니다.
그런데 이미 이런 작업을 편리하게 해주는 도구들이 몇 가지 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;다양한_jdk_설치와_사용을_편하게_하는_도구&quot;&gt;2. 다양한 JDK 설치와 사용을 편하게 하는 도구&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK의 설치와 OS의 명령행에서 사용할 버전을 지정할 때는 아래 도구들을 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-ends grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 1. JDK 설치와 버전 지정에 사용할 수 있는 도구들&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 37.5%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 37.5%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;이름&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;기능&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;사용 가능한 OS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;YUM/APT&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 패키지 관리 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;update-alternatives/alternatives&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 패키지 버전 선택 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Homebrew&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 패키지 관리 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;macOS&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://chocolatey.org/&quot;&gt;Chocolatey&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 패키지 관리 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Windows&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://sdkman.io/&quot;&gt;SDKMAN&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 패키지 관리 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux&lt;br&gt;
macOS&lt;br&gt;
Windows(Cygwin, Git Bash)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/Jabba-Team/jabba&quot;&gt;jabba&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 설치 특화 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux&lt;br&gt;
macOS&lt;br&gt;
Windows&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://www.jenv.be/&quot;&gt;jEnv&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 버전 선택 특화 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux&lt;br&gt;
macOS&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://direnv.net/&quot;&gt;direnv&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;범용 디렉터리별 환경 변수 관리 도구&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Linux&lt;br&gt;
macOS&lt;br&gt;
Windows&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;aptyum&quot;&gt;2.1. APT/YUM&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ubuntu, CentOS 등의 Linux 배포판에서는 해당 OS에 맞도록 빌드한 OpenJDK 배포판을 APT,YUM 등으로 간단히 설치할 수 있도록 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;APT(Advanced Packaging Tool)는 Ubuntu 등 Debian 계열의 리눅스에서 사용할 수 있는 패키지 관리 프로그램입니다.
Ubuntu에서는 APT로 아래와 같이 여러 버전의 JDK를 설치할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo apt install openjdk-8-jdk
sudo apt install openjdk-11-jdk
sudo apt install openjdk-12-jdk&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;YUM(Yellow dog Updater, Modified)은 Red Hat/CentOS 리눅스 배포판에서 사용할 수 있는 패키지 관리자입니다.
아래와 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo yum install java-1.8.0-openjdk-devel.x86_64&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Adopt OpenJDK 배포판은 패키지 저장소를 추가해서 설치할 수 있습니다.
Ubuntu에서는 아래와 같이 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo add-apt-repository ppa:rpardini/adoptopenjdk
sudo apt install adoptopenjdk-11-installer&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치된 JDK의 &lt;code&gt;java&lt;/code&gt; , &lt;code&gt;javac&lt;/code&gt; 도구는  &lt;code&gt;/usr/bin/java&lt;/code&gt; , &lt;code&gt;/usr/bin/javac&lt;/code&gt; 에서 심볼릭 링크로 연결되어 어느 디렉터리에서나 실행될 수 있게 됩니다.
이 심볼릭 링크는 이어서 소개할 update-alternatives / alternatives 도구로 관리할수 있습니다.
&lt;code&gt;JAVA_HOME&lt;/code&gt; 환경 변수는 직접 &lt;code&gt;~/.bashrc&lt;/code&gt; 와 같은 셸별 설정 파일에 넣어줘야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OS에서 기본 제공하는 도구이기에 도구를 위한 별도의 설치 과정이 필요 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 외에도 Maven, Gradle의 설치에도 활용할 수 있는 범용적인 패키지 관리 도구입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SDKMAN/ Jabba에 비하면 다양한 JDK 배포판을 제공하지는 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;update_alternatives_alternatives&quot;&gt;2.2. update-alternatives / alternatives&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;update-alternatives와 alternatives는 여러 버전의 패키지를 관리할 수 있는 Linux에서 제공되는 도구입니다.
여기서는 Ubuntu에서 쓰는 &lt;code&gt;update-alternatives&lt;/code&gt; 를 기준으로 설명하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞서 나온데로 apt 로 설치한 JDK는 &lt;code&gt;/usr/bin/java&lt;/code&gt; 에서 심볼릭 링크로 연결됩니다.
이 심블릭 링크는 &lt;code&gt;/etc/alternatives/java&lt;/code&gt; 를 중간에 거쳐서 실제 설치한 디렉터리로 연결된 다는 것을 아래와 같이 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;➜  ~ ll /usr/bin/java
lrwxrwxrwx 1 root root 22  6월  9 22:20 /usr/bin/java -&amp;gt; /etc/alternatives/java
➜  ~ ll /etc/alternatives/java
lrwxrwxrwx 1 root root 43  6월  9 22:20 /etc/alternatives/java -&amp;gt; /usr/lib/jvm/java-12-openjdk-amd64/bin/java&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;readlink -f /usr/bin/java&lt;/code&gt; 명령으로도 동일한 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 링크는 &lt;code&gt;update-alternatives&lt;/code&gt; 로 관리됩니다. 아래와 같은 명령으로 현재 설치된 버전들과 우선 순위를 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo update-alternatives --display java&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;수동으로 다운로드 압축을 풀어서 설치하거나 SDKMAN, Jabba등으로 설치한 JDK가 있다면 아래 명령으로 &lt;code&gt;update-alternatives&lt;/code&gt; 의 관리대상에 추가할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk1.8.0_31/bin/java 1000&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;심볼릭 링크로 연결되는 버전을 바꾸고 싶다면 아래와 같이 입력합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sudo update-alternatives --config java&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치된 버전을 확인하고 번호를 선택해서 심볼릭 링크를 바꿀 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;There are 4 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                            Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-12-openjdk-amd64/bin/java      1211      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java      1111      manual mode
  2            /usr/lib/jvm/java-12-openjdk-amd64/bin/java      1211      manual mode
  3            /usr/lib/jvm/java-13-openjdk-amd64/bin/java      1211      manual mode
  4            /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java   1081      manual mode

Press &amp;lt;enter&amp;gt; to keep the current choice[*], or type selection number:&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 명령행에서 실행한 &lt;code&gt;java&lt;/code&gt; 가 어느 곳으로 연결될지는 환경변수 &lt;code&gt;PATH&lt;/code&gt; 에 영향을 받습니다.
&lt;code&gt;/usr/bin/java&lt;/code&gt; 보다 더 우선 순위가 높게 먼저 선언된 디렉터리에 &lt;code&gt;java`가 있다면 `update-alternatives&lt;/code&gt; 에서 지정한 java가 실행되지 않을 수도 있습니다.
SDKMAN, Jabba 등을 함께 사용한다면 이 점을 유의해야 합니다.
현재 셸, 디렉터리에서 어느 &lt;code&gt;java&lt;/code&gt; 를 실행하고 있는지는 &lt;code&gt;which java&lt;/code&gt; 로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OS에서 기본적으로 제공하는 도구라서 별도의 설치 과정이 필요하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;YUM/APT 과 자연스럽게 함께 쓰이는 도구입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;심블릭 링크로 셸에서 사용할 디폴트 버전을 지정하는 기능만 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;homebrew&quot;&gt;2.3. Homebrew&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;macOS에서 많이 쓰는 범용 패키지 관리 프로그램입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Homebrew로 AdoptOpen JDK배포판은 아래와 같이 설치할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;brew tap AdoptOpenJDK/openjdk
brew cask install adoptopenjdk11&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 macOS를 써본적이 없어서 Homebrew로 설치하는 방법에 대해서는 &lt;a href=&quot;https://findstar.pe.kr/2019/01/20/install-openjdk-by-homebrew/&quot;&gt;homebrew로 opendjk 설치하기&lt;/a&gt; 글을 참조했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;chocolatey&quot;&gt;2.4. Chocolatey&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Chocolatey는 Windows OS를 위한 패키지 관리자입니다.
Linux에는 APT/YUM, macOS에는 Homebrew가 있다면 Windows에는 Chocolatey가 대표적인 패키지 관리자입니다.
&lt;a href=&quot;https://chocolatey.org/install&quot; class=&quot;bare&quot;&gt;https://chocolatey.org/install&lt;/a&gt; 을 참고해서 설치할수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Chocolatey로 설치가능한 JDK 패키지는 &lt;a href=&quot;https://chocolatey.org/packages?q=jdk&quot; class=&quot;bare&quot;&gt;https://chocolatey.org/packages?q=jdk&lt;/a&gt; 으로 확인하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;:&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jdk/chocolatey-jdk.jpg&quot; alt=&quot;chocolatey-jdk.jpg&quot; title=&quot;Chocolatey 패키지 중에서 JDK로 검색한 결과&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Oracle의 OpenJDK 빌드나 Adopt OpenJDK 배포판 등을 아래와 같이 설치할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Oracle의 OpenJDK 빌드 최신 버전 설치&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;choco install openjdk&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;AdoptOpenJDK 최신 버전 설치&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;choco install adoptopenjdk&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Corretto 11 버전 설치&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;choco install corretto11jdk&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;zulu 최신 버전 설치&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;choco install zulu&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--version&lt;/code&gt; 옵션을 붙이면&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Oracle의 OpenJDK 빌드 11.0.2 버전 설치&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;choco install openjdk --version 11.0.2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 명령이 수행되고 나면 &lt;code&gt;\Program Files\&lt;/code&gt; 디렉터리 아래에 JDK 들이 위치하게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Oracle의 OpenJDK 빌드 : &lt;code&gt;\Program Files\OpenJDK&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Adopt OpenJDK : &lt;code&gt;\Program Files\AdoptOpenJDK&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Corretto : &lt;code&gt;\Program Files\Coretto&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zulu : &lt;code&gt;\Program Files\zulu&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런대 Chocolatey는 여러 JDK 버전을 동시에 쓰는 쓰임새가 우선적으로 고려되지는 않았습니다.
JDK 12.0.1을 설치후에 11.0.2을 뒤에 설치하려고하면, 다운그레이드가 된다는 경고 메시지가 나옵니다.
이럴 때에는 &apos;-sidebyside&apos; 혹은 &lt;code&gt;--force&lt;/code&gt; 등의 옵션을 붙여줘야합니다.
&lt;code&gt;JAVA_HOME&lt;/code&gt; 도 마지막으로 설치한 JDK의 위치로 지정됩니다.
&lt;code&gt;echo %java_home%&lt;/code&gt; 명령으로 이를 확인해 볼 수 있습니다.
여러 배포판을 설치할 경우 &lt;code&gt;PATH&lt;/code&gt; 환경 변수의 값도 새로 설치한 배포판의 &lt;code&gt;%JAVA_HOME%\bin&lt;/code&gt; 디렉터리가 뒤 쪽에 계속 추가만 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK 외에도 Maven, Gradle의 설치에도 활용할 수 있는 범용적인 패키지 관리 도구입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SDKMAN/ Jabba에 비하면 다양한 JDK 배포판을 제공하지는 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;여러 버전을 동시에 설치할 수 있는 동작이 디폴트가 아닙니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;OS 명령행에서 여러 JDK 버전을 함께 사용하려면 direnv등 별도의 프로그램과 함께 쓰는 것이 좋습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;direnv&quot;&gt;2.5. direnv&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://direnv.net/&quot;&gt;direnv&lt;/a&gt; 는 특정 디렉터리와 그 하위 디렉터리에서만 사용할 환경 변수를  지정할 수 있는 도구입니다.
Linux와 macOS에서 사용할 수 있습니다. 설치 방법은 &lt;a href=&quot;https://direnv.net/&quot; class=&quot;bare&quot;&gt;https://direnv.net/&lt;/a&gt; 을 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;direnv에서 참조하는 &lt;code&gt;.envrc&lt;/code&gt; 라는 파일에 PATH, JAVA_HOME 을 아래와 같이 지정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;export JAVA_HOME=/home/benelog/.sdkman/candidates/java/12.0.1.hs-adpt
export PATH=$JAVA_HOME/bin:$PATH&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파일을 처음 생성하거나 변경했을 때에는 &lt;code&gt;direnv allow .&lt;/code&gt; 명령을 한번 내려줘야합니다.
이 파일이 의도하지 않게 생성/수정 되었을 때 보안을 위한 장치입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이후로 이 파일이 있는 디렉터리에 들어가면 이 환경변수가 활성화됩니다.
cd 명령으로 디렉터리에 들어가면 아래와 같은 메시지가 콘솔에 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;direnv: loading .envrc
direnv: export ~JAVA_HOME ~PATH&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보편적으로 사용할 수 있는 도구이기에 &lt;code&gt;JAVA_HOME&lt;/code&gt; 외의 다른 환경 변수도 관리할 수 있습니다.
같은 프로젝트를 하더라도 개발자의 PC마다 달라지는 값이나 테스트를 위한 변수도 &lt;code&gt;.envrc&lt;/code&gt; 에 넣어둘만합니다.
그럴 경우에는 &lt;code&gt;.envrc&lt;/code&gt; 은 &lt;code&gt;.gitignore&lt;/code&gt; 에 추가해서 Git 저장소에는 들어가지 않도록 해야 하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;JAVA_HOME&lt;/code&gt; 이나 &lt;code&gt;PATH&lt;/code&gt; 외의 환경 변수도 관리할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;특정 디렉터리 내에서의 환경 변수 기능만 제공합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jenv&quot;&gt;2.6. jEnv&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jenv.be/&quot;&gt;jEnv&lt;/a&gt; 는 JDK 버전관리만을 위한 전용 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 &lt;code&gt;add&lt;/code&gt; 명령으로 관리할 버전을 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jenv add /usr/lib/jvm/java-11-openjdk-amd64/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt; 로 지정한 디렉터리에서 JDK의 버전을 인식하여 아래와 같은 메시지가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;jenv add 명령의 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;openjdk64-11.0.3 added
11.0.3 added
11.0 added&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치된 버전은 &lt;code&gt;jenv versions&lt;/code&gt; 명령으로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;jenv versions 명령의 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  system
  1.8
  1.8.0.212
* 11.0 (set by JENV_VERSION environment variable)
  11.0.3
  openjdk64-1.8.0.212
  openjdk64-11.0.3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;디폴트로 사용할 버전은 &lt;code&gt;global&lt;/code&gt; 명령으로 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jenv global 11.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;해당 셸에서 임시로 사용할 버전은 &lt;code&gt;shell&lt;/code&gt; 명령으로 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jenv shell 11.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 디렉터리에서 사용할 버전은 &lt;code&gt;local&lt;/code&gt; 명령으로 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jenv local 11.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위와 같이 디렉터리에 지정된 버전은 &lt;code&gt;.java-version&lt;/code&gt; 이라는 파일에 저장됩니다.
다음 번에 같은 디렉터리에서 java를 실행하면 이 파일에 지정된 해당 버전이 선택됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;JAVA_HOME&lt;/code&gt; 환경 변수가 제대로 지정되기 위해서는 jENV의 export plugin을 아래 명령으로 활성화해줘야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jenv enable-plugin export&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jEnv를 다른 도구와 잘 어우러지게 사용하기 위해서는 동작 원리를 알아두는 것이 좋습니다.
jEnv로 JDK 버전을 지정한 후 &lt;code&gt;which java&lt;/code&gt; 로 어느 디렉터리에 있는 &lt;code&gt;java&lt;/code&gt; 와 연결되는지 확인을 해보면
&lt;code&gt;~/.jenv/shims/java&lt;/code&gt; 가 나옵니다. 이 파일의 내용을 보면 실제 설치한 JDK의 &lt;code&gt;java&lt;/code&gt; 가 아닌 셸 스크립트라는 것을 알수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;cat ~/.jenv/shims/java&lt;/code&gt; 명령의 결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;#!/usr/bin/env bash
set -e
[ -n &quot;$JENV_DEBUG&quot; ] &amp;amp;&amp;amp; set -x

program=&quot;${0##*/}&quot;
if [ &quot;$program&quot; = &quot;java&quot; ]; then
  for arg; do
    case &quot;$arg&quot; in
    -e* | -- ) break ;;
    */* )
      if [ -f &quot;$arg&quot; ]; then
        export JENV_DIR=&quot;${arg%/*}&quot;
        break
      fi
      ;;
    esac
  done
fi

export JENV_ROOT=&quot;/root/.jenv&quot;
exec &quot;/root/.jenv/libexec/jenv&quot; exec &quot;$program&quot; &quot;$@&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 다른 도구와 병행해서 사용할 경우, 환경변수 &lt;code&gt;$PATH`에 `~/.jenv/shims/java`가 다른 도구에서 넣어준 JDK와 연결된 경로들보다 앞에 있어야 jEnv에서 설정한 버전대로 `java&lt;/code&gt; 가 실행됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;$JAVA_HOME`도 어떻게 지정되어 있는지 `echo $JAVA_HOME&lt;/code&gt; 로 확인을 해보면 &lt;code&gt;~/.jenv/versions/11.0&lt;/code&gt; 와 같이 지정되어 있습니다.
`~/.jenv/versions/ 디렉터리에 각 버전별로 실제로 JDK가 설처되어있는 디렉터리로의 심볼릭 링크가 들어가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;~/.jenv/versions&lt;/code&gt; 디렉터리 안의 심볼릭 링크&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 1.8 -&amp;gt; /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 1.8.0.212 -&amp;gt; /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 11.0 -&amp;gt; /usr/lib/jvm/java-11-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 11.0.3 -&amp;gt; /usr/lib/jvm/java-11-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   33 Jun 30 17:05 openjdk64-1.8.0.212 -&amp;gt; /usr/lib/jvm/java-8-openjdk-amd64/
lrwxrwxrwx  1 benelog benelog   34 Jun 30 17:08 openjdk64-11.0.3 -&amp;gt; /usr/lib/jvm/java-11-openjdk-amd64/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 jEnv는 여러 배포판을 동시에 설치할 때는 충돌을 일으킬수 있습니다.
예를 들어 Ubuntu 패키지 저장소의 OpenJDK 11을 이미 &apos;jenv add&apos; 로 넣은 다음,
AdoptOpenJDK 11을 추가하면 아래와 같이 이미 존재하는 버전이라는 메시지가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;code&gt;`jenv add /usr/lib/jvm/adoptopenjdk-11-jdk-hotspot&lt;/code&gt; 실행결과&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt; openjdk64-11.0.3 already present, skip installation
 11.0.3 already present, skip installation
 11.0 already present, skip installation&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jEnv는 동일한 JDK 배포판의 여러 버전을 관리하는데 적합합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;다양한 범위(디폴트(global), 디렉터리별, 셸 범위)의 버전 방식을 지원합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;다양한 배포판의 동일한 JDK 버전(예: 11.0.3)을 관리할 수 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;sdkman&quot;&gt;2.7. SDKMAN&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SDKMAN(The Software Development Kit Manager)은 여러 개발도구를 설치할 수 있는 도구입니다.
JDK 뿐만 아니라 Maven, Gradle, Ant, AsciidoctorJ 등 JVM 세계의 다양한 도구들을 설치할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OS별로 SDKMAN을 설치하는 방법은 &lt;a href=&quot;https://sdkman.io/install&quot; class=&quot;bare&quot;&gt;https://sdkman.io/install&lt;/a&gt; 을 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SDKMAN으로 설치할 수 있는 JDK 배포판/버전은 &lt;code&gt;sdk list java&lt;/code&gt; 명령으로 확인할 수 있습니다.
아래와 같이 사용할 수 있는 배포판들과 설치된 버전 등을 표시해 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;================================================================================
Available Java Versions
================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
 AdoptOpenJDK  |     | 12.0.1.j9    | adpt    |            | 12.0.1.j9-adpt
               |     | 12.0.1.hs    | adpt    | installed  | 12.0.1.hs-adpt
               |     | 11.0.3.j9    | adpt    |            | 11.0.3.j9-adpt
               |     | 11.0.3.hs    | adpt    |            | 11.0.3.hs-adpt
               |     | 8.0.212.j9   | adpt    |            | 8.0.212.j9-adpt
               | &amp;gt;&amp;gt;&amp;gt; | 8.0.212.hs   | adpt    | installed  | 8.0.212.hs-adpt
 Amazon        |     | 11.0.3       | amzn    |            | 11.0.3-amzn
               |     | 8.0.212      | amzn    |            | 8.0.212-amzn
 Azul Zulu     |     | 12.0.1       | zulu    |            | 12.0.1-zulu
               |     | 11.0.3       | zulu    |            | 11.0.3-zulu
               |     | 10.0.2       | zulu    |            | 10.0.2-zulu
               |     | 9.0.7        | zulu    |            | 9.0.7-zulu
               |     | 8.0.212      | zulu    |            | 8.0.212-zulu
               |     | 7.0.222      | zulu    |            | 7.0.222-zulu
               |     | 6.0.119      | zulu    |            | 6.0.119-zulu
 Azul ZuluFX   |     | 11.0.2       | zulufx  |            | 11.0.2-zulufx
               |     | 8.0.202      | zulufx  |            | 8.0.202-zulufx
 BellSoft      |     | 12.0.1       | librca  |            | 12.0.1-librca
               |     | 11.0.3       | librca  |            | 11.0.3-librca
               |     | 8.0.212      | librca  |            | 8.0.212-librca
 GraalVM       |     | 19.0.2       | grl     |            | 19.0.2-grl
               |     | 19.0.0       | grl     |            | 19.0.0-grl
               |     | 1.0.0        | grl     | installed  | 1.0.0-rc-16-grl
 SAP           |     | 12.0.1       | sapmchn |            | 12.0.1-sapmchn
               |     | 11.0.3       | sapmchn |            | 11.0.3-sapmchn
 java.net      |     | 14.ea.1      | open    |            | 14.ea.1-open
               |     | 13.ea.25     | open    |            | 13.ea.25-open
               |     | 12.0.1       | open    |            | 12.0.1-open
               |     | 11.0.2       | open    |            | 11.0.2-open
               |     | 10.0.2       | open    |            | 10.0.2-open
               |     | 9.0.4        | open    |            | 9.0.4-open
================================================================================
Use the Identifier for installation:

    $ sdk install java 11.0.3.hs-adpt
================================================================================&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;AdoptOpenJDK HotSpot 배포판 12.0.1 버전을 설치하고 싶다면 아래와 같은 명령을 내립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sdk install java 12.0.1.hs-adpt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;PATH&lt;/code&gt; , &lt;code&gt;JAVA_HOME&lt;/code&gt; 환경변수도 알아서 잘 잡아줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;명령행에서 디폴트로 사용할 JDK 버전은 &lt;code&gt;~/.sdkman/candidates/java/current&lt;/code&gt; 에서 심볼릭 링크로 관리됩니다.
이 링크가 환경변수 &lt;code&gt;$PATH`와 `$JAVA_HOME&lt;/code&gt; 에 추가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 심볼릭 링크는 아래 명령으로 바꿀 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sdk default java 8.0.212.hs-adpt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 셸에서 사용할 버전만 임시로 바꾸고 싶다면 &lt;code&gt;default&lt;/code&gt; 대신 &lt;code&gt;use&lt;/code&gt; 명령을 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;sdk use java 8.0.212.hs-adpt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;다양한 JDK 배포판을 설치할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK 설치와 버전 지정을 하나의 도구로 관리할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;특정 디렉터리에 들어갔을 때 사용할 버전을 자동을 지정하는 기능이 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sdk use&lt;/code&gt;  명령이 jabba의 동일한 기능에 비해 실행 속도가 느립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jabba&quot;&gt;2.8. jabba&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jabba는 JDK의 설치/버전 관리만을 위한 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 OS별 jabba의 설치 방법은 &lt;a href=&quot;https://github.com/Jabba-Team/jabba#installation&quot; class=&quot;bare&quot;&gt;https://github.com/Jabba-Team/jabba#installation&lt;/a&gt; 을 참조합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치할 수 있는 JDK의 배포판은 &lt;code&gt;jabba ls-remote&lt;/code&gt;  명령으로 확인할 수 있습니다.
이중 Amazon에서 제공하는 Corretto 배포판 JDK 11을 설치한다면 아래와 같은 명령을 내립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jabba install amazon-corretto@1.11.0-3.7.1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치된 버전들은 &lt;code&gt;jabba ls&lt;/code&gt; 명령으로 확인할 수 있습니다.
현재 셸에서 사용할 버전은 아래와 같이 지정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;jabba use adopt-openj9@1.12.33-0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;jabba use&lt;/code&gt; 를 실행하면 &lt;code&gt;PATH&lt;/code&gt; 와 &lt;code&gt;JAVA_HOME&lt;/code&gt; 환경변수를 지정한 JDK 버전을 참조할수 있도록 바꾸어줍니다.
&lt;code&gt;echo $PATH&lt;/code&gt; 로 PATH 값을 확인해보면, 가장 앞에 설치한 JDK의 bin 디렉터리를 지정할 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 디렉터리에 `.jabbarc`라는 파일이 있다면, 그 파일에 지정된 버전을 참조할 수 있습니다.
즉 아래와 같이 실행해도 특정 버전을 지정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;echo &quot;adopt-openj9@1.12.33-0&quot; &amp;gt; .jabbarc
jabba use&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음 번에 같은 디렉터리에 들어왔을 떄에는 &lt;code&gt;jabba use&lt;/code&gt; 만 간단하게 실행해서 같은 효과를 낼 수 있습니다.
direnv나 jEnv를 쓸 때처럼 디렉터리에 들어가면 자동으로 환경변수를 바꾸어주는 기능은 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 셸범위의 JDK 버전만 지정한다는 점이 jabba의 장점이나 단점입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;다른 도구와 충돌없이 쓰기에 좋습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jabba use&lt;/code&gt; 명령이 SDKMAN의 &lt;code&gt;sdk use&lt;/code&gt; 에 비해 실행 속도가 빠릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;단점&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;디폴트 버전 지정이 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;디렉터리별 버전 비전 기능이 완전 자동이 아닙니다. 해당 디렉터리에서 &lt;code&gt;jabba use&lt;/code&gt; 를 한번 입력해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;무엇을_어떻게_사용할_것인가&quot;&gt;3. 무엇을 어떻게 사용할 것인가?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 다양한 도구 중 어떤 것을 골라 쓸지는 개발장비의 OS와 필요한 범위에 따라서 결정해야할 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우선 다양한 배포판의 JDK를 쓰는 것까지 필요가 없다면 아래 정도의 조합을 고려할만합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Windows : Chocolatey + direnv&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux : APT/YUM + update-alternatives + jEnv (또는 direnv)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;macOS : Homebrew + jEnv(또는 direnv)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[Mac에 Java 여러 버전 설치] 글에서는 Homebrew로 Oracle JDK를 설치하고 jEnv와 함께 사용하는 사례가 정리되어 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Amazon Corretto, GraalVM 등 다양한 배포판의 여러버전을 설치해보고 싶다면 SDKMAN이나 jabba를 함께 쓰는 것을 추천합니다.
각 도구들이 지원하는 배포판은 아래와 같습니다. (2019년 7월1일 기준)&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-ends grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 2. JDK 설치 도구들이 지원하는 배포판&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 66.6667%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;이름&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;지원하는 JDK 배포판&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;YUM/APT&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OS 배포판별 OpenJDK &lt;strong&gt;(*1)&lt;/strong&gt;&lt;br&gt;
AdoptOpen JDK&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Homebrew&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Oracle JDK&lt;br&gt;
Adopt OpenJDK&lt;br&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://chocolatey.org/&quot;&gt;Chocolatey&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Oracle JDK&lt;br&gt;
Oracle의 OpenJDK 빌드 &lt;strong&gt;(*2)&lt;/strong&gt;&lt;br&gt;
Adopt OpenJDK&lt;br&gt;
Amazon Corretto&lt;br&gt;
Zulu OpenJDK&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://sdkman.io/&quot;&gt;SDKMAN&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Oracle의 OpenJDK 빌드 &lt;strong&gt;(*2)&lt;/strong&gt;&lt;br&gt;
Adopt OpenJDK&lt;br&gt;
Amazon Corretto
GraalVM CE&lt;br&gt;
Zulu OpenJDK&lt;br&gt;
Zulu OpenJDK + OpenJFX&lt;br&gt;
SapMachine&lt;br&gt;
Liberica JDK&lt;br&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/Jabba-Team/jabba&quot;&gt;jabba&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Oracle JDK&lt;br&gt;
Oracle의 OpenJDK 빌드 &lt;strong&gt;(*2)&lt;/strong&gt;&lt;br&gt;
Adopt OpenJDK&lt;br&gt;
Amazon Corretto&lt;br&gt;
GraalVM CE&lt;br&gt;
Zulu OpenJDK&lt;br&gt;
IBM SDK&lt;br&gt;
OpenJDK 참조 구현체&lt;br&gt;
OpenJDK + Shenandoah GC&lt;br&gt;
Liberica JDK&lt;br&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;(*1)&lt;/strong&gt; : 해당 OS 배포판을 위해 빌드된 OpenJDK 배포판입니다. OS의 배포판을 관리하는 업체/커뮤니티에서 관리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;(*2)&lt;/strong&gt; : &lt;a href=&quot;https://jdk.java.net/&quot; class=&quot;bare&quot;&gt;https://jdk.java.net/&lt;/a&gt; 에서 다운로드 받을 수 있는 OpenJDK 배포판입니다. 출시 후 6개월까지만 최신 버전이 업데이트됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위에 정리한 것처럼 SDKMAN과 jabba가 많은 JDK 배포판을 지원합니다.
둘다 Adopt OpenJDK, Amazon Corretto, GraalVM CE, Zulu 등 주목받는 주요 배포판은 모두 포함하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SDKMAN에서는 제공하는 반면  jabba에는 없는 배포판은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Zulu OpenJDK + OpenJFX&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SapMachine&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jabba에서는 제공하는 반면 SDKMAN에는 없는 배포판은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;IBM SDK&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenJDK 참조 구현체&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jdk.java.net/java-se-ri/8&quot; class=&quot;bare&quot;&gt;https://jdk.java.net/java-se-ri/8&lt;/a&gt; 등 에서 받을수 있는 배포판 입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenJDK + Shenandoah GC&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SDKMAN과 jabba는 JDK 설치와 버전 지정 기능을 동시에 제공합니다.
그런데 jenv등 다른 도구에서 제공하는 버전 지정 기능을 완정히 제공하지는 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-ends grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 3. JDK 버전 지정 기능&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;도구&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;디폴트&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;디렉터리별&lt;/th&gt;
&lt;th class=&quot;tableblock halign-center valign-top&quot;&gt;셸 범위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;update-alternatives/
alternatives&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;X&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;X&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://sdkman.io/&quot;&gt;SDKMAN&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;X&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://github.com/shyiko/jabba&quot;&gt;jabba&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;X&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;△&lt;strong&gt;(*3)&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://www.jenv.be/&quot;&gt;jEnv&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://direnv.net/&quot;&gt;direnv&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;X&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;O&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-center valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;X&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;(*3)&lt;/strong&gt; : jEnv나 direnv처럼 디렉터리에 들어가면 자동으로 특정 JDK 버전이 선택되는 방식은 아니기 때문에 △로 표기했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 SDKMAN이나 jabba는 다른 도구와 조합해서 사용하면 더욱 편리하게 쓸 수 있습니다.
그런데 앞서 언급했듯이 jEnv는 SDKMAN이나 jabba와 함께 쓰기에는 적합하지 않습니다.
&lt;code&gt;$PATH&lt;/code&gt; 환경 변수에 지정된 경로의 순서에 따라서 여러 도구의 버전 지정 결과가 의도하지 않게 덮어 써질수 있습니다.
즉 SDKMAN에 지정한 경로가 앞에 있으면 jEnv에서 지정한 JDK 버전이 인식되지 않는 것처럼 보일수도 있습니다.
그리고 jabba로는 여러 배포판의 JDK 11.0.3 을 설치할 수 있지만 jEnv에서는 &apos;jenv add&apos; 로 같은 버전(11.0.3)의 다른 배포판을 추가할 수 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 다양한 배포판을 설치하고자 할때는 SDKMAN(또는 jabba) +  direnv 조합을 추천합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 이 도구들을 쓰는 환경은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;각각 다른 JDK 버전을 쓰는 여러 프로젝트의 소스를 고칩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;업무 혹은 취미로 JDK의 여러 배포판/ 버전을 설치해서 차이가 있는지 확인하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(예: 포함된 ca-cert 목록 비교, GraalVM으로 네이티브 이미지 만들기 시도)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;회사의 업무용 노트북과 집에 있는 PC에서 Ubuntu 19.04를 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이에 따라 저는 아래와 같이 도구를 조합해서 쓰고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK 설치에는 APT, SDKMAN, jabba를 다 사용해 보고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;사용할 버전을 선택할 때는&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;디폴트 버전은 SDKMAN으로 지정합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SDKMAN을 설치하면 SDK에서 관리하는 패키지들이 &lt;code&gt;/usr/bin&lt;/code&gt; 보다 앞에 오기 때문입니다. 디폴트 버전은 자주 바꾸진 않기 때문에 굳이 이를 조정하진 않았습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;특정 디렉터리에서 사용한 버전을 지정할 때는 direnv를 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;셸에서 일시적으로 사용할 버전을 지정할 때는 SDKMAN, jabba를 씁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>정적 사이트로 블로그 마이그레이션</title>
      <link>https://blog.benelog.net//migration-to-static-site.html</link>
      <pubDate>Mon, 6 May 2019 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">migration-to-static-site.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#블로그를_이전한_동기&quot;&gt;1. 블로그를 이전한 동기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#요구사항&quot;&gt;2. 요구사항&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#선택한_기술&quot;&gt;3. 선택한 기술&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#정적_사이트_생성기&quot;&gt;3.1. 정적 사이트 생성기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#git_저장소_빌드배포&quot;&gt;3.2. Git 저장소, 빌드/배포&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#편집기&quot;&gt;3.3. 편집기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#마이그레이션_프로그램&quot;&gt;3.4. 마이그레이션 프로그램&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#마치며&quot;&gt;4. 마치며&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;블로그를_이전한_동기&quot;&gt;1. 블로그를 이전한 동기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 이 블로그를 &lt;a href=&quot;http://www.egloos.com&quot;&gt;Egloos&lt;/a&gt;에서 커스텀 URL(blog.benelog.net)을 설정하여 쓰고 있었습니다.
Egloos는 무난한 블로깅 플랫폼입니다. 그러나 개발과 관련된 글을 쓰면서 코드를 편집하기에는 저한테는 Egloos에서 제공하는 에디터가 잘 맞지가 않았습니다.
점점 글을 안 쓰게 되었고, 이 블로그는 갱신되지 않는 옛날 글만 가득하게 되었습니다.
공유할 글이 생길 때는 &lt;a href=&quot;https://gist.github.com/&quot; class=&quot;bare&quot;&gt;https://gist.github.com/&lt;/a&gt; 를 이용하기도 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이전처럼 가끔씩이라도 블로그에 글을 올리려면 블로그 플랫폼을 바꿔야겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;요구사항&quot;&gt;2. 요구사항&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;블로그 플랫폼을 선택하는데 염두에 둔 조건은 아래와 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;기존의 포스트의 주소가 동일하게 유지되어야 한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;외부에서 걸린 링크가 깨어지지 않아야한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;기존 블로그에 달린 댓글을 그대로 옮겨올 수 있어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;다른 서비스/도구로 다시 옮겨가기가 쉬워야한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AsciiDoc으로 포스트를 작성할 수 있어야한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;선택한_기술&quot;&gt;3. 선택한 기술&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 요구사항을 충족시키는 다음과 같은 서비스/도구들을 선택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;정적 사이트 생성기 : &lt;a href=&quot;https://jbake.org/&quot;&gt;JBake&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;콘텐츠 저장소 : GitHub&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;빌드/배포 : &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;댓글 : &lt;a href=&quot;https://help.disqus.com/&quot;&gt;Disqus&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;방문자 통계 집계 : &lt;a href=&quot;https://analytics.google.com/analytics/web/&quot;&gt;Google Analytics&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;새로운 블로그는 정적 사이트 생성기(Static Site Generator)로 Git 저장소에 있는 AsciiDoc 형식의 콘텐츠를 HTML 파일로 만들어서 배포하는 구조입니다.
덕분에 Egloos로부터의 마이그레이션 작업이 더 수월했고 더 좋은 서비스가 있을 때 쉽게 이전을 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;콘텐츠 저장소는 GitHub외에도 Gitlab, Bitbucket 등의 대안이 있고, 콘텐츠 빌드 배포 플랫폼도 다양합니다.
필요하다면 더 좋은 기능이 있는 서비스를 유료로 사용할 수도 있습니다.
콘텐츠 편집기도 여러가지를 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;사용할 기술을 선택하면서 했던 고민들은 아래에 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;정적_사이트_생성기&quot;&gt;3.1. 정적 사이트 생성기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정적 사이트 생성기로 JBake를 선택한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;필수요구사항이였던 AsciiDoc 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JVM 기반&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;일할 때도 쓰는 환경이라서 문제를 만났을 때 해결하는 시간이 짧고, 직접 고쳐서 쓸 수 있는 가능성도 높습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gradle 빌드 구성 등 이미 알고 있는 지식과 결합시키기에도 편했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JBake로 만들어진 템플릿인 &lt;a href=&quot;https://github.com/manikmagar/jbake-future-imperfect-template&quot;&gt;jbake-future-imperfect-template&lt;/a&gt;의 편의성&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Disqus와 Google Analytics를 설정파일을 1줄씩을 고쳐서 간단히 적용할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;비슷한 고민을 하실 분들이 참고할만한 링크들을 정리해봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;정적 사이트 생성기가 정리된 사이트&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.staticgen.com/&quot; class=&quot;bare&quot;&gt;https://www.staticgen.com/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://staticsitegenerators.net/&quot; class=&quot;bare&quot;&gt;https://staticsitegenerators.net/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/briandominick/e5754cc8438dd9503d936ef65fffbb2d&quot;&gt;AsciiDoc을 지원하는 정적 사이트 생성기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JVM 기반의 정적 사이트 생성기&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://orchid.netlify.com/&quot;&gt;Orchid&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.opoopress.com/&quot;&gt;OpooPress&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/wiztools/stagen&quot;&gt;StaGen&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://sysgears.com/work/grain/&quot;&gt;Grain&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://nakkaya.com/static.html&quot;&gt;nakkaya staic&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;미래에 JBake를 다른 도구로 교체한다면 아래 2개의 도구를 가장 우선적으로 고려할 예정입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://orchid.netlify.com/&quot;&gt;Orchid&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JVM 기반이면서 AsciiDoc 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;현대적인 기술 스택인 &lt;a href=&quot;https://jamstack.org/&quot;&gt;JAMstack&lt;/a&gt; 활용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.netlifycms.org/&quot;&gt;Netlify CMS&lt;/a&gt;와 연동됨&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;빠른 빌드 성능&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;템플릿, 편집과 배포에 대한 생태계가 풍부함&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://gohugo.io/tools/frontends/&quot; class=&quot;bare&quot;&gt;https://gohugo.io/tools/frontends/&lt;/a&gt; 참조&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.netlifycms.org/&quot;&gt;Netlify CMS&lt;/a&gt;와도 연동됨&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;git_저장소_빌드배포&quot;&gt;3.2. Git 저장소, 빌드/배포&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git 저장소는 많이 쓰이는 GitHub를 무난하게 선택했습니다.
AsciiDoc으로 작성된 포스트의 원본은 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/src/content&quot;&gt;github/benelog/blog/src/content&lt;/a&gt; 에서 이력을 관리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에는 &lt;a href=&quot;https://travis-ci.org/&quot;&gt;Travis CI&lt;/a&gt;와 Githb Page를 활용해서  빌드/배포를 구성하고 커스텀 도메인과 HTTPS를 적용하려고 했었습니다.
그래서 이 블로그는 &lt;a href=&quot;https://benelog.github.io/blog&quot; class=&quot;bare&quot;&gt;https://benelog.github.io/blog&lt;/a&gt; 로도 접근이 가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;빌드 스크립트는 &lt;a href=&quot;https://github.com/benelog/blog/blob/main/build.gradle&quot;&gt;build.gradle&lt;/a&gt;에서 확인이 가능합니다.
Travis CI 설정인 &lt;code&gt;.travis.yml&lt;/code&gt;은 아래와 같이 단순합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;yml&quot;&gt;language: java
script: ./gradlew gitPublishPush&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 작업을 실행하기 위해서는 &lt;a href=&quot;https://github.com/settings/tokens&quot; class=&quot;bare&quot;&gt;https://github.com/settings/tokens&lt;/a&gt; 에서 발급받은 Personal Access Token을
Tracis CI의 저장소별 설정에서 &lt;code&gt;GRGIT_USER&lt;/code&gt; 라는 환경변수로 지정을 해주어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/blog-migration/travis-ci-settings.png&quot; alt=&quot;travis-ci-settings&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최종적으로는 지원하는 기능이 더 풍부한 &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netflify&lt;/a&gt;을 이 도메인에는 사용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gradle로 만든 빌드 스크립트를 지정하고 Domain을 지정하고 HTTPS를 적용하는 작업까지 편하게 할 수 있었습니다.
Netflify에는 Name 서버 기능까지 있어서 사용하던 Name 서버도 이 서비스로 이전을 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/blog-migration/netflify.png&quot; alt=&quot;netflify&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Netflify에서는 &lt;code&gt;./gradlew bake&lt;/code&gt; 로 빌드를 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/blog-migration/netflify-deployment.png&quot; alt=&quot;netflify&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 블로그의 JBake 설정으로는 &lt;a href=&quot;https://blog.benelog.net/904735.html&quot; class=&quot;bare&quot;&gt;https://blog.benelog.net/904735.html&lt;/a&gt; 와 같이 HTML 확장자를 붙인 주소를 씁니다.
Egloos의 이전 포스트들은 &lt;a href=&quot;https://blog.benelog.net/904735&quot; class=&quot;bare&quot;&gt;https://blog.benelog.net/904735&lt;/a&gt; 와 같이 HTML 확장자가 없게 되어 있습니다.
Netflify와 Github pages에서는 디폴트로 아무런 설정이 없어도 HTML 확장자가 없는 주소를 HTML 확장자가 붙은 주소와 동일하게 취급을 해주어서
URL의 하위 호환성을 쉽게 유지할 수 있었습니다.
JBake의 설정으로 HTML 확장자를 제외하는 것도 시도해보았지만, 태그 링크 같은 블로그의 다른 요소의 링크들과 함께 잘 어우러지는 것이 쉽지 않았습니다.
호스팅 서비스 단에서 연결해 해주는 방법이 더 간편하고 부작용이 없어서 최종적으로 그에 의지하도록 했습니다.
즉, 기존 주소 연결을 위해서 아무런 설정도 안 했다는 이야기입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정적 페이지를 호스팅할수 있는 다른 서비스를 찾는다면 아래의 링크들을 참조하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://about.gitlab.com/product/pages/&quot;&gt;Gitlab Pages&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://surge.sh/&quot;&gt;Surge&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.aerobatic.com/&quot;&gt;Aerobatic&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://zeit.co/now&quot;&gt;Now&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/s3/&quot;&gt;Amazon S3&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://firebase.google.com/docs/hosting/&quot;&gt;Firebase Hosting&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;편집기&quot;&gt;3.3. 편집기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;긴 글을 편집할 때는 IntelliJ Community Edition을 사용했습니다.
평소 개발할 때의 편집방식, 단축키와 Git 저장소 연동 방식을 그대로 활용할 수 있습니다.
&lt;code&gt;Find in Path (Ctrl + Shift + F)&lt;/code&gt; , &lt;code&gt;Replace in Path (Ctrl + Shift + R)&lt;/code&gt; 으로 여러 파일에 걸쳐 문자열을 검색하고 치환하는 기능은 마이그레이션 된 콘텐츠를 정리할 때 많은 도움이 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/7391-asciidoc&quot;&gt;AsciiDoc plugin&lt;/a&gt;을 설치하면
HTML으로 렌더링될 모습을 동시에 보면서 편집을 할 수 있고 이미지 파일을 참조할 때는 자동완성이 되기도합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/blog-migration/intellij-asciidoc.png&quot; alt=&quot;IntelliJ로 AsciiDoc 편집&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;간단한 글을 쓰거나 수정할 때는 Github에서 바로 온라인으로 편집을 할 생각입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 정적 사이트 생성기를 기반으로 온라인 에디터를 제공하는 서비스들은 아래와 같은 것들이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://appernetic.io/&quot;&gt;Appernetic.io&lt;/a&gt; : Hugo 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://forestry.io/&quot;&gt;Forestry&lt;/a&gt; : Jekyll, Hugo, Vuepress 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.netlifycms.org/&quot;&gt;Netlify CMS&lt;/a&gt; : Hugo, Gatsby, Middleman 등 지원&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;마이그레이션_프로그램&quot;&gt;3.4. 마이그레이션 프로그램&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마이그레이션 프로그램의 소스와 사용법은 &lt;a href=&quot;https://github.com/benelog/egloos-migration/&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/egloos-migration/&lt;/a&gt; 을 참고하시기 바랍니다.
이것도 제가 알고 있는 기술들을 활용해서 Kotlin + Spring 으로 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;콘텐츠를 담은 파일을 생성할 때 Kotlin의 문자열 문법이 유용할 것으로 예상해서 언어는 Kotlin을 선택했습니다. 아래와 같은 코드입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;kotlin&quot;&gt;    private fun convert(post: EgloosPost): String {
        val asciiDoc = converter.convert(post.content)
        return &quot;&quot;&quot;= ${post.title}
${post.nick}
${post.createdAt.format(formatter)}
:jbake-type: post
:jbake-status: published
:jbake-tags: ${post.tags}
:idprefix:

$asciiDoc
&quot;&quot;&quot;
        // String을 inputStream으로 읽어서쓰니 trimIndent가 안 먹음.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 프로그램을 부분적으로 실행하고 검증해보기 위해서 Spring Batch를 썼습니다.
원래 Spring Batch는 대용량 데이터를 메모리에 다 올리지 않고 처리할 때 적합한 프레임워크입니다.
제 블로그의 포스트는 100개 정도로 작은 데이터인데 여기에 Spring Batch를 쓰는 것은 어울리지 않을 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, Spring Batch의 ItemReader, ItemProcessor, ItemWriter 구조에 맞춰서 프로그램을 만들고
이 기능 단위로 테스트를 해보면 것이 마이그레이션 프로그램을 만드는 효율적인 방법이라고 판단했습니다.
Spring Batch를 쓰지 않았어도 비슷한 기능 단위로 프로그램을 쪼갯을 것 같았기에 이미 익숙한 프레임워크의 인터페이스를 활용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;부분적인 기능의 테스트들은 &lt;a href=&quot;https://github.com/benelog/egloos-migration/tree/master/src/test/kotlin/net/benelog/blog/migration/etl&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/egloos-migration/tree/master/src/test/kotlin/net/benelog/blog/migration/etl&lt;/a&gt; 을 참조하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;마치며&quot;&gt;4. 마치며&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오랜 숙원 사업이였던 블로그 이전을 마쳐서 뿌듯합니다.
다른 개발자들에게 도움이 될만한 글을 가끔씩이라도 써보겠습니다.
이전에 썼던 글들도 틈틈히 최신화하려고 합니다.
할 작업들은 &lt;a href=&quot;https://github.com/benelog/blog/issues&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/blog/issues&lt;/a&gt; 에 기록해두고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Robolectric을 활용한 안드로이드 쾌속 테스팅</title>
      <link>https://blog.benelog.net//robolectric.html</link>
      <pubDate>Mon, 10 Oct 2016 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">robolectric.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2014년 8월1일 &lt;a href=&quot;http://d2.naver.com/helloworld/870369&quot;&gt;제9회 오픈 세미나 in 대구&lt;/a&gt; 행사에서 한 발표입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예제 코드들은 당시의 Rolbectric의 최신 버전을 기준으로 한 것이라 현재의 최신 버전에서는 그대로 실행되지 않을 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오늘 발표에서는 테스트 프레임워크인 Robolectric을 사용하면서 실무에서 얻었던 경험을 공유하고자합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;먼저 테스트코드란 무엇인지와 안드로이드에서 테스트 작성을 어렵게 하는 난관등을 말씀드리고 Robolectric을 활용하는 방법을 소개하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;테스트_코드란&quot;&gt;테스트 코드란?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참석하신 분 중에서 JUnit(제이유닛)에 대해서 한번이라도 들어보신 분은 손을 들어보시겠습니까? 이중에 Junit을 실제로 써보신분은 얼마나 되시나요? Android에서 JUnit으로 테스트를 시도해보신 분은 계신가요? 경험을 하신 정도가 다양하기 때문에 우선 오늘 다룰 테스트 코드란 무엇인지를 한번 정리하고 시작을 하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;단순히 말해서 테스트 코드는 &apos;검증&amp;#8217;을 위한 코드입니다. 다음은 이미지로딩 라이브러리인 Universal ImageLoader의 소스 중 테스트 코드입니다. github에서 전체소스를 확인하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Test
public void testSchemeFile() throws Exception {
    String uri = &quot;file://path/on/the/device/1.png&quot;;
    Scheme result = Scheme.ofUri(uri);
    Scheme expected = Scheme.FILE;
    assertThat(result).isEqualTo(expected);
}

@Test
public void testSchemeUnknown() throws Exception {
    String uri = &quot;other://image.com/1.png&quot;;
    Scheme result = Scheme.ofUri(uri);
    Scheme expected = Scheme.UNKNOWN;
    assertThat(result).isEqualTo(expected);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( Android Universal Image Loader의 &lt;a href=&quot;https://github.com/nostra13/Android-Universal-Image-Loader/blob/master/library/test/com/nostra13/universalimageloader/core/download/BaseImageDownloaderTest.java&quot;&gt;BaseImageDownloaderTest&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;골뱅이 Test가 붙은 애너테이션으로 실행될 코드를 지정하고 assert 구문으로 기대하는 결과를 명시합니다. 이 코드는 uri를 파싱해서 Scheme (스킴)이라는 Enum(이늄)클래스를 만든 결과가 기대대로 FILE이나 UNKNOWN인지를 확인하고 있습니다. 이 코드는 오늘 이야기할 Robolectric으로 만들어졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트를 도와주는 프레임워크도 굉장히 많습니다. 앞에서도 언급드렸듯이, 테스트를 실행하는데는 JUnit이 가장 많이 쓰입니다. 안드로이드 SDK에서도 이를 바탕으로 한 테스트 프레임워크를 제공하고 있습니다. 그리고 테스트에 쓰이는 가짜 객체를 흔히 목(Mock)이라고 부르는데, Mockito, JMock, PowerMOck과 같은 라이브러리들이 있습니다. 안드로이드를 위한 테스트 프레임워크에도 오늘 다룰 Robolectric을 비롯해 Robotium, Spoon, Robospock 등이 존재합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오늘 설명을 이어가는데 혼동을 줄이기 위해 유의해야할 개념을 몇가지 말씀드리겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째, JUnit으로 하는 테스트라고 전부 유닛테스트는 아니라는 것입니다. JUnit에 유닛(Unit)이라는 이름이 들어가서 생기는 혼동입니다. 테스트 Functional 테스트 (혹은 시스템 테스트)도 JUnit으로 작성하는 경우도 많습니다. 오늘 발표에서는 이를 엄밀히 구분하지는 않고 폭넓게 &apos;테스트 코드&amp;#8217;라는 말로만 칭하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 테스트 코드를 작성하는 작업을 &apos;TDD를 한다&amp;#8217;라고 한마디로 말하기는 어렵다는 것입니다. TDD는 테스트를 작성하는 하나의 방식입니다. TDD 기법은 테스트를 먼저 작성하고, 테스트를 통과시키는 코드를 구현한 후 리팩토링을 하는 절차를 거칩니다. 오늘 발표에서는 TDD 같은 절차와 상관없이 테스트를 작성하는 라이브러리 활용법에 대해서 주로 말씀드리겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;왜 이런 테스트를 만드는지에 대한 의문을 가지실분도 있을 듯합니다. 간단히 설명드리면, 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째, 디버깅 편의성을 위해서입니다. 테스트 코드 작성에 능숙해지면 실제 애플리케이션을 실행하고 수동으로 반복 테스트하는것보다 훨씬 빠르고 정교하게 내가 짠 코드의 동작을 확인하고 오류를 수정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 설계를 개선하기 위해서입니다. 테스트 하기에 쉬운 구조의 코드는 역할과 책임이 잘 나누어진 코드입니다. 그런 코드는 재활용하고 기능을 추가하거나 버그를 발견하기에도 편합니다. 테스트를 의식하면서 개발을 하면 그런 구조의 코드를 작성하는데 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;셋째, 테스트 자체가 동작하는 예제이자 명세가 됩니다. 다른 사람이 애플리케이션이나 라이브러리의 전체를 실행시키지 않아도 코드가 실행된 결과를 이해할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;넷째, 반복적으로 수행할 회귀테스트를 자동화합니다. 앞으로 기능을 추가하거나 코드를 개선할 때 든든한 버팀목이 되고 시간을 아껴줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다섯째, 개발 작업에 더 집중하게 해줍니다. 테스트를 통과한다는 명확한 목표가 있고, 이를 빠른 시점에 명확하고 신호로 알려주고 작업의 난이도와 간격은 스스로 적당하게 조절할수 있습니다. 심리학에서 말하는 몰입경험의 조건과 일치합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;안드로이드_테스트의_장벽&quot;&gt;안드로이드 테스트의 장벽&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다면 이런 테스트의 장점을 안드로이드에서도 행복하게 누릴수가 있을까요? 불행히도 많은 장벽이 있다는 것을 경험했습니다. 그 이유를 몇가지 나누어서 말씀드리겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째, Mock을 쓰기 어려운 기본 프레임워크 구조입니다. 안드로이드에서 굉장히 많이 쓰이는 getViewById, getSystemService 같은 코드는 상위클래스에 있는 메서드를 호출하는 구조입니다. 이런 형태는 가짜 클래스인 Mock을 주입하기가 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 빈약한 기본 Mock클래스입니다. android.test.mock 패캐지 아래에는 MockContext, MockApplication, MockResource 등의 많은 클래스들이 있지만, 이들은 UnsupportedOperationException을 던지는 껍데기일 뿐입니다. 필요한 동작은 다음과 같이 직접 override해서 구현해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static public class MockServiceContext extends MockContext {
    @Overrride
    public getSystemService(String name){
        ……
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;셋째, 기본적으로 제공되는 Instrumentation Test를 쓰는것도 배우기가 쉽지않습니다. 예를 들면 Activity를 테스트할때 ActivityTestCase, ActivityUnitTestCase, ActivityInstrumentationTestCase2의 세 가지 클래스 중 어느것을 써야할까를 하려면 많은 것을 알고 있어야합니다. 그리고 ActivityUnitTestCase에서 Dialog생성 등에 Event가 전달되면 BadToken Exception이 발생한다거나, ActivityInstrumentationTestCase2에서 Dialog 객체를 생성 후 dismiss() 메서드를 호출하지 않으면 leak window Exception이 발생하는등 부딛히는 예외상황도 많습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;넷째, UI 테스트 본연의 어려움이 있습니다. 안드로이드 코드는 역할상 UI 생성과 이벤트를 다루는 코드의 비중이 높습니다. 이는 웹애플리케이션 등 다른 플랫폼에서도 테스트하기 어려운 분야입니다. UI 객체의 속성은 자주 바뀌고 익명 클래스 등을 통해서 처리되는 이벤트는 Mock 객체로 바꾸고 추적하기가 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다섯째, 느린 테스트 실행 속도입니다. 단 한줄을 고쳐도 패키징 &amp;#8594; 설치 &amp;#8594; 실행 싸이클을 거칩니다. 이 부분이 테스트의 장점을 다 말아먹는 가장 치명적인 단점입니다. 요즘 PC나 단말이 많이 빨라졌고 Genymotion같은 빠른 에뮬레이터도 활용할 수 있어서 많이 나아졌지만, 그래도 실행싸이클의 특성상 개선에 한계가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;robolectric_활용법&quot;&gt;Robolectric 활용법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저도 Android의 기본 Instrutation테스트를 시도해보다 앞에서 말씀드린 이유로 많은 좌절을 느꼈습니다. 그래서 이를 개선하는 기술로 Robolecric을 시도해봤고, 어느정도 노하우를 쌓았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Robolectric은 배포, 설치가 필요없도록 PC의 JVM에서 안드로이드 코드를 실행해줍니다. 아마 한두번쯤을 만나셨을 메시지일듯한데, 이클립스 같은 IDE안에서 안드로이드 코드를 바로 돌리면 &apos;java.lang.RuntimeException: Stub!?&apos; 에러가 발생합니다. Robolectric은 Android SDK가 제공하는 클래스를 가로채어서 서 JVM에서 ANdroid 코드를 실행해도 저런 에러가 나지 않는 가짜코드를 실행합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 프로젝트는 Github에서 활발하게 개발되고 있습니다. ActionbarSherlock으로 유명한 JakeWharton도 주요 커미터입니다. 174명의 기여자가 참여했고, 저도 그 174명 중의 한명이기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;릴리즈 노트를 보시면 아시겠지만, 이 프레임워크는 꾸준히 발전하고 있습니다. 최근에는 KitKat에 추가된 API를 지원하는 작업도 진행되고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Android를 만든 구글에서도 Robolectric의 1점대의 버전을 자체 포크해서 쓴 흔적이 Android 코드저장소에 남아있습니다. 이렇게 포크로 그치지 않고 구글에서도 같이 Robolectric 2점대 버전의 개발에 참여했으면 더 좋지 않을까 하는 아쉬움이 남습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;물론 Dalvik이나 Art같은 Android 본연의 환경이 아닌 JVM에서 실행되다는 점 때문에 이 라이브러리의 한계는 있습니다. 그리고 Android SDK의 모든 영역을 SDK 출시 즉시에 제공하지도 못합니다. 그렇지만 Robolectric의 한계를 잘 인식하고 효율적으로 테스트할수 있는 부분에 집중을 한다면 앱이나 라이브러리를 개발하는데 많은 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;몇가지 대표적인 사용사례를 들어보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;로그를_system_out으로_출력하기&quot;&gt;로그를 System.out으로 출력하기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우선 LogCat으로 출력되는 로그를 Log를 System.out으로 출력하려면 아래와 같이 구현을 하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SystemUtilsTest {
    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;android.util.Log를 이용한 클래스를 JVM에서 바로 실행가능합니다. java.lang, java.util등 기본 JDK에도 동일한 이름으로 존재하는 클래스를 주로 쓰는 유틸리티 클래스를 만들어도 Log를 찍는 코드가 중간에 들어가있으면 이를 Dalvik에서만 실행해야했습니다. Robolect은 그런 코드도 JVM에서 실행되도록 하며 위와 같이 ShawdowLog클래스에 stream속성을 System.out으로 지정하면 System.out.println으로 찍는것과 유사하게 PC의 표준출력에서 로그메시지를 확인할수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;단말기의_정보_변경&quot;&gt;단말기의 정보 변경&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;종종 Build.VERSION.SDK_INT 변수의 값을 참조해서 SDK의 버전별로 분기처리를 해야하는 코드가 있습니다. Robolectric에서는 이런 상수값도 아래와 같이 조작을 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;	Robolectric.Reflection.setFinalStaticField(Build.VERSION.class, &quot;SDK_INT&quot;, Build.VERSION_CODES.JELLY_BEAN);&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 기법은 Http호출을 하는 클라이언트에서 userAgent에 단말의 정보를 조합해서 넣어하는 경우를 테스트하는 경우에 유용하게 썼습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Activity 클래스는 ActivityController라는 클래스를 통해 생성할 수 있습니다. 아래 코드는 스크린밝기를 지정하는 유틸리티는 테스트하는 코드입니다. 이 소스코드는 github에서 전체를 확인해보실수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Test
public void shouldChangeScreenBrightness() {
    TestActivity activity = createActivity(TestActivity.class);
    float brightness = 0.5f;
    ScreenUtils.setScreenBrightness(activity, brightness);

    LayoutParams lp = activity.getWindow().getAttributes();

    assertThat(lp.screenBrightness, is(brightness));
}

private &amp;lt;T extends Activity&amp;gt; T createActivity(Class&amp;lt;T&amp;gt; activityClass) {
        ActivityController&amp;lt;T&amp;gt; controller = Robolectric.buildActivity(activityClass);
        controller.create();
        return controller.get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( &lt;a href=&quot;https://github.com/naver/android-utilset/blob/master/UtilSet/test/com/navercorp/utilset/ui/ScreenUtilsTest.java&quot;&gt;ScreenUtilsTest&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DisplayMetricsDensity 속성은 직접 &lt;a href=&quot;https://github.com/robolectric/robolectric/blob/master/src/main/java/org/robolectric/Robolectric.java&quot;&gt;org.robolectric.Robolectric&lt;/a&gt;의 set메서드로 지정할 수 있습니다. 아래는 DP와 Pixel을 전환하는 코드를 예제로 들어봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class PixelUtilsTest {
    private Context context;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        this.context = Robolectric.application;
    }

    @Test
    public void shouldGetDpFromPixel(){
        Robolectric.setDisplayMetricsDensity(1.5f);
        int dp = PixelUtils.getDpFromPixel(context, 50);
        assertThat(dp, is(33));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( &lt;a href=&quot;https://github.com/naver/android-utilset/blob/master/UtilSet/test/com/navercorp/utilset/ui/PixelUtilsTest.java&quot;&gt;PixelUtilsTest&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 클래스의 Setter 메서드를 살펴보면 그밖에도 다양하게 테스트를 위한 가짜 객체를 설정하는 기능을 찾으실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;단말의_sdk_정보를_원하는_값으로&quot;&gt;단말의 SDK 정보를 원하는 값으로&lt;/h3&gt;

&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;system_서비스의_결과를_원하는_값으로&quot;&gt;System 서비스의 결과를 원하는 값으로&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;몇가지 예를 들었는데, Robolectric을 결국 어떻게 활용할 것이 좋을까요? JVM에서 테스트해도 동일한 결과를 보장하는 문자열, 날짜 처리, 프로토콜 파싱 영역에서 이득이 많습니다. 주로java.lang, java.util , java.io 패키지가 다루는 영역에 우선 집중하기는 것이 좋습니다. 처음부터 Activity, Fragment같은 UI영역까지 포함한 통합 테스트에 너무 많은 기대를 걸면 오히려 어려울 수 있습니다. Utility 클래스부터 우선 적용해보면서 점점 영역을 넓혀가시기를 권장드립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Robolectric의 버전 2.3부터는 실제 Sqlite 구현체를 이용하기 시작했습니다. 이 버전부터는 DB관련 테스트도 JVM에서 시도해볼만합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;당연히 Robolectric으로 테스트를 포기할 영역도 많습니다. 노하우가 쌓이면 이를 의식해서 테스트의 이득이 높은 영역을 분리해서 설계할 수 있습니다. 이는 재활용/기능 추가/버그 발견에도 좋은 구조가 될것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;코드_기여&quot;&gt;코드 기여&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;계속 발전하고 있는 프레임워크이기 때문에 Robolectric에는 미비한 기능도 많습니다. 테스트 대상인 ANdroid 자체가 계속 변화하고 있어서 더욱 그렇기도 합니다. Robolectric은 Github에 올라간 오픈소스 프로젝트이기 때문에 누구나 코드 기여를 할 수 있습니다. 저도 3번 정도 Pull request를 날렸는데 그 경험을 공유해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에는 Javadoc의 오타부터 수정해봤습니다. &lt;a href=&quot;https://github.com/robolectric/robolectric/pull/804&quot;&gt;Pull request 번호 804번&lt;/a&gt;에서는 ShadowCookieManager의 javadoc에서 TelephonyManager로 적힌 단어를 CookieManager 로 수정했습니다. 주석을 한번이라도 본 사람이면 할 수 있는 아주 단순한 수정이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한번 해보고나니 조금 더 어려운 기여를 해보고 싶어졌습니다. 프로젝트를 진행하다가 Robolectric의 ShawdowCookieManager가 실제 android의 CookieManager의 동작과는 많이 다르다는 것을 발견했습니다. Robolectric 2.2까지는 단순히 HashMap에 key,value를 저장하는 수준이였습니다. Expires같은 속성이 들어가면 실제 SDK와 다르게 동작함. 아래 코드는 테스트가 실패합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;	cookieManager.setCookie(httpUrl, &quot;name=value; Expires=Wed, 11 Jul 2035 08:12:26 GMT&quot;);
	assertThat(cookieManager.getCookie(httpUrl)).isEqualTo(&quot;name=value&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pull request 번호 853번에서는 이를 실제와 비슷하게 재구현했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 과정이 흥미로웠기 때문에 잠시 설명드리면, 먼저 실제 단말에서의 동작을 AndroidTestCase로 확인했습니다. ( &lt;a href=&quot;https://gist.github.com/benelog/7655764&quot;&gt;https://gist.github.com/benelog/7655764&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들면 아래와 같이 removeExpireCookie를 호출했을 때 Expires값이 지나간 쿠키값은 삭제하는 동작을 확인해봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;CookieManager cookieManager;

public void setUp() {
    Context context = getContext();
    CookieSyncManager.createInstance(context);
    cookieManager = CookieManager.getInstance();
    cookieManager.removeAllCookie();
}

public void testRemoveExpiredCookie() {
    cookieManager.setCookie(url, &quot;name=value; Expires=Wed, 11 Jul 2035 10:18:14 GMT&quot;);
    cookieManager.setCookie(url, &quot;name2=value2; Expires=Wed, 13 Jul 2011 10:18:14 GMT&quot;);
    cookieManager.removeExpiredCookie();
    assertEquals(&quot;name=value&quot;, cookieManager.getCookie(url));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 유사한 테스트 케이스를 Robolectric으로 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;CookieManager cookieManager = Robolectric.newInstanceOf(CookieManager.class);

@Test
public void shouldRemoveExpiredCookie() {
    cookieManager.setCookie(url, &quot;name=value; Expires=Wed, 11 Jul 2035 10:18:14 GMT&quot;);
    cookieManager.setCookie(url, &quot;name2=value2; Expires=Wed, 13 Jul 2011 10:18:14 GMT&quot;);
    cookieManager.removeExpiredCookie();
    assertThat(cookieManager.getCookie(url)).isEqualTo(&quot;name=value&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 테스트를 통과시키는 ShadowCookieManager를 구현하여 Pull request를 날렸습니다. Robolectric에 들어갈 코드를 Robolecric으로 검증한 셈입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마지막으로 ShawdowProcess 구현한 코드입니다. 이 클래스로 android.os.Process.myPid()에서 나오는 값을 가짜로 지정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Test
public void shouldBeTrueWhenThisContextIsForeground(){
    int pid = 3;
    ShadowProcess.setPid(pid);
    createRunningAppProcessInfo(pid);
    boolean foreground = ActivityUtils.isContextForeground(context);
    assertThat(foreground, is(true));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구글의 Android 소스 저장소의 Robolectric fork판에도 유사한 클래스가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 클래스는 &lt;a href=&quot;https://github.com/robolectric/robolectric/pull/861/&quot;&gt;Pull request 861번&lt;/a&gt; 으로 던져서 반영되었습니다. 중간에 이 클래스가 없으면 어떻게 되냐고 물어보길래 자세히 설명하려고 노력했던 과정이 재미있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코드 기여에 유의할 점도 있습니다. Merge를 받아줄 주요 커미터들이 작업하기 편하게 Pull request를 하는 브랜치는 계속 master의 최신 commit으로 맞춰서 rebase를 해줘야합니다. 제가 한 요청들도 다른 요청에 밀려서 merge가 안 되고 있었는데, 계속적으로 rebase를 하고 있으니 그 정성을 봐서도 merge를 해준것 같기도합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그외에는 컨티리뷰션 가이드를 참조하시면 됩니다. 대표적인 내용을 소개드리면, Indent에는 탭대신 공백 2칸을 쓰는등 컨벤션을 맞춰야하고, 적절한 테스트 코드를 같이 commit을 해야합니다. 앞에서 나온 CookieManager 사례를 참고하셔도 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하자면 다음과 같습니다. Android 테스트는 난관이 많습니다. 특히 느린 실행속도가 치명적입니다. 여기서 Robolectric이 도움이 됩니다. 우선은 문자열, API 파싱. 유틸리티등 테스트하기 쉬운 영역부터 시도해볼만하고, 궁극적으로는 설계개선을 고민하는 것이 좋습니다. 코드 기여도 어렵지 않은, 기여자에게 관대한 프로젝트입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오늘 발표와 관련해서 helloworld 블로그에 게시된 &lt;a href=&quot;http://helloworld.naver.com/helloworld/342818&quot;&gt;Android에서 @Inject, @Test&lt;/a&gt;글도 참고하실만합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;발표자료&quot;&gt;발표자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.slideshare.net/deview/5robolectric&quot;&gt;https://www.slideshare.net/deview/5robolectric&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;iframe src=&quot;//www.slideshare.net/slideshow/embed_code/key/hzukqnu9TZiiOJ&quot; width=&quot;595&quot; height=&quot;485&quot; frameborder=&quot;0&quot; marginwidth=&quot;0&quot; marginheight=&quot;0&quot; scrolling=&quot;no&quot; style=&quot;border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;&quot; allowfullscreen&gt; &lt;/iframe&gt; &lt;div style=&quot;margin-bottom:5px&quot;&gt; &lt;strong&gt; &lt;a href=&quot;//www.slideshare.net/deview/5robolectric&quot; title=&quot;[D2 오픈세미나]5.robolectric 안드로이드 테스팅&quot; target=&quot;_blank&quot;&gt;[D2 오픈세미나]5.robolectric 안드로이드 테스팅&lt;/a&gt; &lt;/strong&gt; from &lt;strong&gt;&lt;a href=&quot;https://www.slideshare.net/deview&quot; target=&quot;_blank&quot;&gt;NAVER D2&lt;/a&gt;&lt;/strong&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>&apos;네이버를 만든 기술, 읽으면서 배운다 - 자바편&apos; 출간</title>
      <link>https://blog.benelog.net//3134907.html</link>
      <pubDate>Wed, 25 Feb 2015 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3134907.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/book-cover/naver-java.jpg&quot; alt=&quot;cover&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( 이미지 출처 : &lt;a href=&quot;http://wikibook.co.kr/how-naver-works-java/&quot; class=&quot;bare&quot;&gt;http://wikibook.co.kr/how-naver-works-java/&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;네이버의 개발자 블로그인 헬로월드 ( &lt;a href=&quot;http://helloworld.naver.com&quot; class=&quot;bare&quot;&gt;http://helloworld.naver.com&lt;/a&gt; )에 공개되었던 글을 중심으로 책이 출판됩니다. 자바의 핵심영역을 다룬 17개의 글을 묶었습니다. 출판을 위해 새로 쓰여진 글도 있고, 사내에서만 공유되었던 글들도 재발굴했고, 이미 공개된 글도 최신 내용을 반영해 다듬었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자바개발의 A부터 Z까지 다 다루는 책은 아니지만, API를 설계할 때 고민해야할 요소, 반복해서 문제를 겪을 만한 부분, 장애 해결/분석 경험의 액기스를 담았습니다. 네이버의 주요 서비스를 개발한 담당자가 문제를 해결한 사례를 정리한 글도 있고, 플랫폼개발, 기술 지원조직인 웹플랫폼개발랩, 성능엔지니어링랩, 생산성혁신랩의 개발자들이 반복해서 전파해야할 지식을 효율적으로 공유하기 위해 쓴 글도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에는 &lt;a href=&quot;http://www.yes24.com/24/goods/1941571?scode=032&amp;amp;OzSrank=5&quot;&gt;조엘이 엄선한 소프트웨어 블로그 베스트 29선&lt;/a&gt;처럼, 온라인의 글을 오프라인으로 옮기는 성격의 책이 될것이라고 예상했었습니다. 그런데 생각보다 출판에 이르기까지는 많은 노력이 들어갔습니다. 글이 쓰여진 시점이 제 각각 이였기 때문에 현시점에 맞춰서 고칠 내용도 많았고, 사내에서만 공유되던 글은 서비스명, 담당자명, 부서명등이 들어간 문장은 바꿔야했습니다. 다양한 저자가 쓴 글이였지만 용어를 일관되게 맞추려고 노력했습니다. 기술문서팀의 담당자분께서 많은 수고를 해주셨습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오류가 있어도 바로 고치면 되는 인터넷 페이지가 아닌, 종이로 찍혀나오는 책에 들어갈 글을 쓰는 부담은 생각보다 컸습니다. 이미 공개된 글도 여러 번 더 신중하게 검토를 했습니다. 블로그에 올릴 때는 &apos;아직 정확한 답은 찾지 못했다.&apos; 정도로 대충 쓰고 넘어갔었던, 타임존데이터베이스에서 우리나라 시간대 정보가 역사적 사실과 맞지 않았던 문제를 더 깊이 파악한 것이 그 예입니다. 책을 쓰면서 IANA ( Internet Assigned Numbers Authority)에 이 오류를 수정한 패치를 전달해서 JDK에도 반영되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;결국 종이로 찍혀나오게 되었으니, Helloworld의 글들을 좋아하셨던 많은 분들에게 소장할 가치가 있는 책으로 남았으면 좋겠습니다. 그 바램을 담아서 서문와 아래와 같이 적었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞으로 자바 10, 자바 11이 나오고 시간이 흐르면 더 최신 정보를 담은 책이 나올 것이다. 그래도 이 시대의 자바 기술과 네이버에서 일한 개발자의 노력을 담은 타입캡슐이 돼 오랜 시간이 지난 후에도 누군가의 책장에 이 책이 꽂혀 있으면 좋겠다. 그것이 인터넷에 있던 글을 종이로 옮긴 가장 큰 의미가 아닐까 생각한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책이 호응이 얻는다면, 바쁜 시간을 쪼개어서 글을 썼던 저자에게 응원이 되고, 예비 저자에게도 용기를 주어 앞으로 더 좋은 글을 외부로 공개하는데 힘을 보탤 것이라 믿습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;목차&quot;&gt;목차&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;글 하나하나가 독립적이기 때문에 관심있는 주제부터 읽으셔도 되지만, 가장 겉으로 들어난 영역인 API부터 시작해서 JVM내부, 분석도구, Garbarge Collection, DB연결까지 이어지는, 더 안쪽과 JVM 뒤쪽의 영역으로 흐름이 이어지도록 목차를 잡았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1부_자바의_api_이해하기&quot;&gt;1부 : 자바의 API 이해하기&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;01장: 자바의 날짜와 시간 API - 정상혁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;02장: 자바의 HashMap은 어떻게 작동하는가? - 송기선&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;03장: 자바에서 외부 프로세스를 실행할 때 - 정상혁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;04장: 람다가 이끌어 갈 모던 자바 - 정상혁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2부_문제_분석과_사례&quot;&gt;2부 : 문제 분석과 사례&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;05장: JVM 이해하기 - 박세훈&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;06장: 스레드 덤프 분석하기 - 구태진&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;07장: 자바 애플리케이션 분석을 위한 BTrace - 이상민, 정상혁&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;08장: 하나의 메모리 누수를 잡기까지 - 김민수, 김택수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;09장: 고맙다 JVM, 사과해라 JVM 크래시 - 강경태&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;3부_가비지_컬렉션&quot;&gt;3부 : 가비지 컬렉션&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;10장: 자바 가비지 컬렉션의 작동 과정 - 이상민&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;11장: 가비지 컬렉션 모니터링 방법 - 이상민, 송기선&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;12장: 가비지 컬렉션 튜닝 - 이상민&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;13장: 자바의 Reference 클래스와 가비지 컬렉션 - 박세훈&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;14장: 가비지 컬렉션과 Statement Pool - 최동순&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;15장: 아파치 MaxClients와 톰캣의 Major GC - 최동순&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;4부_데이터베이스_연결_설정&quot;&gt;4부 : 데이터베이스 연결 설정&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;16장: JDBC의 타임아웃 이해하기 - 강운덕&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;17장: Commons DBCP 이해하기 - 최동순, 강운덕, 정상혁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>시간대 DB에서 우리나라 시간의 오류</title>
      <link>https://blog.benelog.net//3120317.html</link>
      <pubDate>Mon, 3 Nov 2014 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3120317.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;변경이력&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2015/02/13&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;tzdata2014j에 반영 사실 갱신&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;북한의 시간대에 대한 진행 상황 설명&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2014년 2월, 회사의 기술블로그인 &lt;a href=&quot;http://helloworld.naver.com에&quot; class=&quot;bare&quot;&gt;http://helloworld.naver.com에&lt;/a&gt; &lt;a href=&quot;http://helloworld.naver.com/helloworld/64560&quot;&gt;Java의 날짜와 시간 API&lt;/a&gt;이라는 글을 기고한 적이 있습니다. 그 글을 쓰던 도중에 우리나라의 타임존 데이터에 대한 몇가지 의문을 가지게 되었지만 완벽히 해결하지는 못했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;얼마 전에 이 문제를 좀 더 깊이 파악을 해보았고, 원천 데이터인 IANA Timezone 데이터베이스에 패치를 전달해서 반영되었습니다. 조사과정에서 1920년대부터 1999년까지의 과거 뉴스를 조회하는 네이버의 뉴스라이브러리 서비스 ( &lt;a href=&quot;http://newslibrary.naver.com/&quot; class=&quot;bare&quot;&gt;http://newslibrary.naver.com/&lt;/a&gt;) 가 큰 도움이 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 오류는 tzdata2014j에 반영되고, 이를 참조하는 Java, Android, FreeBSD에서도 2014년 11월경에 반영되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://permalink.gmane.org/gmane.comp.time.tz/9111&quot;&gt;tzdata2014j의 릴리즈노트&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.java.net/browse/JDK-8064560&quot;&gt;JDK-8064560&lt;/a&gt; : Open JDK의 반영. TZUpdater 1.4.10가 오류가 수정된 이 데이터를 참조하고, JDK 8u40, JDK
7u80, JDK 7u75등 여러 버전에 백포트 패치&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://android.googlesource.com/platform/bionic.git/+/b11d8e057c86c3926128af9d07180d9328e144c6&quot;&gt;Android에 반영 Commit&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://lists.freebsd.org/pipermail/svn-src-stable-10/2014-November/003831.html&quot;&gt;FreeBSD에 반영 Commit&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;플랫폼별로 반영시점은 다르겠지만, 다른 OS에서 이를 반영할 것으로 예상합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현시점에서는 드물것 같지만, 혹시 우리나라의 1912 ~ 1980년대의 섬머타임과 시간대 변경에 영향받은 프로그램을 만드셨던 분이 있다면 참고로 알아둘만 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;섬머타임의_오류_발견&quot;&gt;섬머타임의 오류 발견&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음 발견한 오류는 1988년의 섬머타임이 시작된 시간이였습니다. 아래 자료에 따르면 이 해의 섬머타임은 5월 8일 새벽 2시부터 시작되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&apos;한국 표준시&apos;, 위키백과, &lt;a href=&quot;http://ko.wikipedia.org/wiki/한국_표준시&quot; class=&quot;bare&quot;&gt;http://ko.wikipedia.org/wiki/한국_표준시&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1988050700099215012&amp;amp;editNo=1&amp;amp;printCount=1&amp;amp;publishDate=1988-05-07&amp;amp;officeId=00009&amp;amp;pageNo=15&amp;amp;printNo=6822&amp;amp;publishType=00020&quot;&gt;&apos;내일부터 서머타임&amp;#8230;&amp;#8203; 새벽2시를 3시로&apos;&lt;/a&gt;, 매일경제, 1988. 5. 7.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 Java프로그램으로는 1988년 5월 7일 23시의 1시간 후가 5월8일 1시인것으로 나와서 00시를 기점으로 섬머타임이 적용되어 있습니다. 아래 테스트는 아직 오류수정이 정식 릴리즈되지 않은 지금 시점에서는 통과합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Test
public void shouldGetAfterOneHour() {
    TimeZone seoul = TimeZone.getTimeZone(&quot;Asia/Seoul&quot;);
    Calendar calendar = Calendar.getInstance(seoul);
    calendar.set(1988, Calendar.MAY , 7, 23, 0);
    String pattern = &quot;yyyy.MM.dd HH:mm&quot;;
    String theTime = toString(calendar, pattern, seoul);
    assertThat(theTime).isEqualTo(&quot;1988.05.07 23:00&quot;);
    calendar.add(Calendar.HOUR_OF_DAY, 1);
    String after1Hour = toString(calendar, pattern, seoul);
    assertThat(after1Hour).isEqualTo(&quot;1988.05.08 01:00&quot;);}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(자세한 설명은 &lt;a href=&quot;http://helloworld.naver.com/helloworld/645609&quot;&gt;Java의 날짜와 시간 API&lt;/a&gt;,전체 소스는 &lt;a href=&quot;https://github.com/benelog/blog/blob/main/examples/java-date-time/jdk7-date-time/src/test/java/com/nbp/weblab/date/OldJdkDateTest.java&quot;&gt;OldJdkDateTest.java)&lt;/a&gt; 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;시간대 변경에 대한 정보는 윈도우즈, 안드로이드, OSX, 리눅스, Java, 오라클 등 거의 모든 플랫폼에서 Internet Assigned Numbers Authority (IANA)라는 조직에서 관리하는 시간대 데이터베이스를 원천으로 참조합니다. 처음에는 이 타임존 데이터베이스의 오류일지 아니면 다른 이유가 있을지 확신을 하지 못했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;오류의_역사&quot;&gt;오류의 역사&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Helloworld에 글이 나간 후에 이응준님께서 알려주셔서 우리나라의 섬머타임을 기록한 사람이 누구인지 알게 되었습니다. Github에 올라간 &lt;a href=&quot;https://github.com/eggert/tz의&quot; class=&quot;bare&quot;&gt;https://github.com/eggert/tz의&lt;/a&gt; 커밋로그를 바탕으로 이를 자세히 분석해봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우리나라의 섬머타임에 대한 기록은 &apos;Arthur David Olson&amp;#8217;의 &lt;a href=&quot;https://github.com/eggert/tz/commit/79373b10463c3b59a1b2af49491dc308efbad09d&quot;&gt;1988년 1월 3일의 커밋&lt;/a&gt;에 아래와 같이 처음으로 등장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;# Republic of Korea. According to someone at the Korean Times in San Francisco,# Daylight Savings Time was not observed until 1987. He did not know# at what time of day DST starts or ends.# Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/SRule ROK 1987 max - May Sun&amp;lt;=14 2:00 1:00 DRule ROK 1987 max - Oct Sun&amp;lt;=14 3:00 0 S&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주석을 봐서는 섬머타임의 시작시기도 정확히 몰랐던 사람의 증언을 참고로 한 듯합니다. 그리고 1987년 이전에는 우리나라에 섬머타임이 없었다는 이야기도 사실과 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 코드로는 1987년부터 섬머타임이 계속되고 있다고 정의되었습니다. 1987,1988년에 우리나라에서 섬머타임이 실행되었으니 commit시점에서는 적어도 이 년도에 대해서는 맞는 데이터였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 1988년 이후로도 우리나라에서는 섬머타임이 계속되어 있는것처럼 한동안 유지가 됩니다. 1993년에 이르러서야 이 데이터는 정정됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/eggert/tz/commit/61315cadc3d0e8b4ef559539502a606cf58fc0f7&quot;&gt;1993년 11월23일의 커밋&lt;/a&gt;으로 다음의 날짜가 다시 반영됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Rule ROK 1960 only - May 15 0:00 1:00 DRule ROK 1960 only - Sep 13 0:00 0 SRule ROK 1987 1988 - May Sun&amp;lt;=14 0:00 1:00 DRule ROK 1987 1988 - Oct Sun&amp;lt;=14 0:00 0 S&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 commit은 아래 2가지 오류를 담고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1987~1988년도의 섬머타임은 시작시간 2시부터인데 0시부터로 표기되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;새로 추가한 1960년의 섬머타임은 실제로는 5월1일부터 9월18일까지였습니다. 위키페디아와 옛날신문의 자료가 일치합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1960050100209103019&amp;amp;editNo=2&amp;amp;printCount=1&amp;amp;publishDate=1960-05-01&amp;amp;officeId=00020&amp;amp;pageNo=3&amp;amp;printNo=11713&amp;amp;publishType=00010&quot;&gt;썸머타임 1일부터&lt;/a&gt;, 동아일보, 1960.05.01&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1960091800209103010&amp;amp;editNo=2&amp;amp;printCount=1&amp;amp;publishDate=1960-09-18&amp;amp;officeId=00020&amp;amp;pageNo=3&amp;amp;printNo=11853&amp;amp;publishType=00010&quot;&gt;없어지는 섬머타임&lt;/a&gt;, 동아일보, 1960.09.18&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이외에도 이 Commit은 우리나라 시간대 변경에 대한 많은 오류를 포함하고 있습니다. 섬머타임 외의 오류는 나중에 다시 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주석으로 볼때 위의 &lt;a href=&quot;https://github.com/eggert/tz/commit/61315cadc3d0e8b4ef559539502a606cf58fc0f7&quot;&gt;1993년 11월23일의 커밋&lt;/a&gt;은 &lt;a href=&quot;http://en.wikipedia.org/wiki/Thomas_G._Shanks&quot;&gt;Thomas G. Shanks&lt;/a&gt;의 The International Atlas의 제3판에 있는 내용을 반영한것을 보입니다. 주석에도 아래와 같이 미국이외의 타임존 정보는 별다른 명시가 없다면 이 책을 참고로 했다고 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;# A good source for time zone historical data outside the U.S. is# Thomas G. Shanks, The International Atlas (3rd edition),# San Diego: ACS Publications, Inc. (1991).# Except where otherwise noted, it is the source for the data below.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금 이 책은 &lt;a href=&quot;http://www.amazon.com/International-Atlas-6th-Thomas-Shanks/dp/0935127887/&quot;&gt;6판&lt;/a&gt;까지 나와있고, 이후의 commit에서도 5판,6판을 따라서 수정한 내용이 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 이후 &lt;a href=&quot;https://github.com/eggert/tz/commit/da23b4d3897fd8a9e302ca0e85b25ce921f95ec7&quot;&gt;2012년 7월 18일의 커밋&lt;/a&gt;이 한번더 섬머타임 데이터를 수정했습니다. 1987년, 1988년의 표현규칙을 바꾼것으로 근본적인 오류가 수정되지는 않았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Rule ROK 1987 1988 - May Sun&amp;gt;=8    0:00 1:00 DRule ROK 1987 1988 - Oct Sun&amp;gt;=8    0:00 0 S&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1960년 이전의 데이터까지 포함한다면, IANA 데이터베이스에서 우리나라의 섬머타임이 제대로 반영된 적은 한번도 없었던 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;패치_전달와_반영&quot;&gt;패치 전달와 반영&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;조사결과 섬머타임의 오류를 확신하고, 이를 수정하는 패치파일을 직접 만들어서 시간대데이터를 관리하는 IANA에 메일(&lt;a href=&quot;mailto:tz@iana.org&quot;&gt;tz@iana.org&lt;/a&gt; )로 보냈습니다. 여러 옛날 신문들을 많이 찾아본결과 &lt;a href=&quot;http://ko.wikipedia.org/wiki/한국_표준시&quot;&gt;위키페이디아의 &apos;한국표준시&amp;#8217;페이지&lt;/a&gt;의 정보가 신뢰할만하다고 판단했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1948.06.01. 00:00 ~ 1948.09.13. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1949.04.03. 00:00 ~ 1949.09.11. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1950.04.01. 00:00 ~ 1950.09.10. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1951.05.06. 00:00 ~ 1951.09.09. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1955.05.05. 00:00 ~ 1955.09.09. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1956.05.20. 00:00 ~ 1956.09.30. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1957.05.05. 00:00 ~ 1957.09.22. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1958.05.04. 00:00 ~ 1958.09.21. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1959.05.03. 00:00 ~ 1959.09.20. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1960.05.01. 00:00 ~ 1960.09.18. 00:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1987.05.10. 02:00 ~ 1987.10.11. 03:00&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1988.05.08. 02:00 ~ 1988.10.09. 03:00&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들면 1948년의 정보는 &lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1948060100209202008&amp;amp;editNo=1&amp;amp;printCount=1&amp;amp;publishDate=1948-06-01&amp;amp;officeId=00020&amp;amp;pageNo=2&amp;amp;printNo=7607&amp;amp;publishType=00020&quot;&gt;1948년 6월1일자 동아일보 기사&lt;/a&gt;에서 확인할수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;패치절차는 시간대데이터베이스의 소스에 있는 &lt;a href=&quot;https://github.com/eggert/tz/blob/master/CONTRIBUTING&quot;&gt;CONTRIBUTING&lt;/a&gt;파일에 잘 설명되어 있습니다. 정식절차와는 별도로 github에도 올려봤습니다. ( &lt;a href=&quot;https://github.com/eggert/tz/pull/9&quot; class=&quot;bare&quot;&gt;https://github.com/eggert/tz/pull/9&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;얼마 후 제가 보낸 패치를 포함하는 &lt;a href=&quot;https://github.com/eggert/tz/commit/929c14a3d8ce505a9b8fb7099bee3a03ccf7e8e4&quot;&gt;2014년 10월30일의 Commit&lt;/a&gt;이 올라왔습니다. &apos;Unreleased, experimental changes&amp;#8217;라는 문구가 포함되었지만, 이를 뒤집는 증거가 발견되지 않는한 정식릴리즈에 포함될 것으로 예상합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IANA쪽에서 이 수정을 받아준 Paul Eggert은 제가 섬머타임 변경의 근거로 보낸 &lt;a href=&quot;http://ko.wikipedia.org/wiki/한국_표준시&quot;&gt;위키페이디아의 &apos;한국표준시&amp;#8217;페이지&lt;/a&gt;를 보고 우리나라의 시간대 변경시점에 대한 오류도 추가로 수정을 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;시간대_변경시점의_오류&quot;&gt;시간대 변경시점의 오류&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에 보낸 패치에는 포함되지 못했지만 섬머타임 외에도 우리나라 시간대 변경에 대한 의문도 있었습니다. &quot;yyyy.MM.dd HH:mm (Z)&quot;을 포맷으로 해서, 1954년, 1961년, 1968년의 특정시간과 그 때와 UTC와의 차이를 출력해보면, 아래와 같이 나옵니다. (소스는 &lt;a href=&quot;https://github.com/benelog/blog/blob/main/examples/java-date-time/jdk7-date-time/src/main/java/com/nbp/weblab/date/TimeZoneChangePoint.java&quot;&gt;TimeZoneChangePoint.java&lt;/a&gt; 참조 )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;1954.03.20 22:59 (+0900)1954.03.20 23:00 (+0800)1961.08.09 23:59 (+0800)1961.08.10 00:30 (+0830)1968.09.30 23:59 (+0830)1968.10.01 00:30 (+0900)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 소스의 결과는 &lt;a href=&quot;https://github.com/eggert/tz/commit/61315cadc3d0e8b4ef559539502a606cf58fc0f7&quot;&gt;1993년 11월23일의 수정&lt;/a&gt; 때 반영된 타임존DB의 정보에 의지합니다. 위의 결과라면 우리나라의 시간대 변경시점은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1954년 : UTC+0900 &amp;#8594; UTC+0800&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1961년 : UTC+0800 &amp;#8594; UTC+0830&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1968년 : UTC+0830 &amp;#8594; UTC+090&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 과거 신문에서 확인한 역사적 사실은 아래와 같습니다. 위키페디아의 내용과도 일치합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1954년 : UTC+0900 &amp;#8594; UTC+0830&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1954032100209203002&amp;amp;editNo=1&amp;amp;printCount=1&amp;amp;publishDate=1954-03-21&amp;amp;officeId=00020&amp;amp;pageNo=3&amp;amp;printNo=9522&amp;amp;publishType=00020&quot;&gt;&apos;오늘은 춘분 시간도 광복&apos;&lt;/a&gt;, 동아일보, 1954.03.21&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1954032100329203006&amp;amp;editNo=1&amp;amp;printCount=1&amp;amp;publishDate=1954-03-21&amp;amp;officeId=00032&amp;amp;pageNo=3&amp;amp;printNo=2462&amp;amp;publishType=00020&quot;&gt;&apos;입일일은 춘분, 표준시간광복의 날&apos;&lt;/a&gt;. 경향신문, 1954.03.21&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1961년 : UTC+0830 &amp;#8594; UTC+0900&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1961080900329203009&amp;amp;editNo=2&amp;amp;printCount=1&amp;amp;publishDate=1961-08-09&amp;amp;officeId=00032&amp;amp;pageNo=3&amp;amp;printNo=4796&amp;amp;publishType=00020&quot;&gt;&apos;구일밤 통금 싸이렌 불면 시계 바늘을 12시반으로&lt;/a&gt;, 경향신문, 1961.8.09&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://newslibrary.naver.com/viewer/index.nhn?articleId=1961081000209204005&amp;amp;editNo=2&amp;amp;printCount=1&amp;amp;publishDate=1961-08-10&amp;amp;officeId=00020&amp;amp;pageNo=4&amp;amp;printNo=12179&amp;amp;publishType=00020&quot;&gt;&apos;새 표준자오선이란?&apos;&lt;/a&gt;, 동아일보, 1961.08.10&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉 현재의 시간대데이터로는 1961~1968년사이는 아예 우리나라의 시간대가 잘못 계산되어 나온다는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 부분은 섬머타임이 반영되는 것을 보고 조금 더 조사를 한 후에 추가 패치를 하려고 생각했었습니다. 기존 데이터가 그렇게까지 다 틀렸다는 것이 믿기가 어려웠고, 우리나라의 시간대 정보에 대한 거의 모든것을 한번에 고치기가 조심스러웠기 때문입니다. 그런데 Paul Eggert가 먼저 적극적으로 반영해주었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Paul Eggert는 이와 더불어 &lt;a href=&quot;http://ko.wikipedia.org/wiki/한국_표준시&quot;&gt;위키페이디아의 &apos;한국표준시&amp;#8217;페이지&lt;/a&gt;에 따르면 1912년에 UTC+0900로 변경이 있었는데, 1910년에도 같은 변경이 있었던것으로 기록된 부분이 혼동된다며 이를 명확히 확인해주었다면 좋겠다고 했습니다. 위키페디아에서 1910년도 변경의 근거로 든 &apos;여적 표준시 변경, 경향신문, 2000-08-14.&apos;라는 자료는 현재 인터넷으로 찾을 수 없어서 대신 여러 기록을 확인해보았습니다. 많은 자료가 1912년에 변경되었다는것으로 일치했고, 1910년도의 변경기록은 누군가가 한일합방 연도와 혼동한것이 아닐까하는 의견을 답장으로 보냈습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;북한의_타임존_데이터&quot;&gt;북한의 타임존 데이터&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또하나 의문이였던 점은 &lt;a href=&quot;https://github.com/eggert/tz/commit/61315cadc3d0e8b4ef559539502a606cf58fc0f7&quot;&gt;1993년 11월23일의 커밋&lt;/a&gt;으로 북한의 시간대가 1961년에 UTC+0900으로 변경되었다는 내용입니다. 그때 남한 쪽에서 시간대 변경이 있었는데, 당시 신문을 다 찾아봐도 남북한이 동시에 추진을 했다는 내용은 없었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Paul Eggert도 이를 이상하게 여겨 일단은 북한쪽은 1940년대 이후로 변화가 없는것으로 가정했다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;While we&amp;#8217;re in the neighborhood, it&amp;#8217;s completely implausible that Pyongyang faithfully mimicked Seoul time during and after the Korean war (which is what Shanks says), so let&amp;#8217;s remove that obviously-bogus guess.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저도 답장으로 북한쪽의 변경에 대한 의미있는 기록을 찾지 못했고, Paul Eggert의 가정에 동의한다는 내용을 보냈습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;tzdata2014j버전대로라면 1954년과 1961년 사이 서울과 평양사이에는 30분의 시차가 존재합니다. 이 것이 역사적 사실과 부합하는지 알아내려고 계속 알아보고 있는 중입니다. 현재 한국표준과학연구원과 통일부, 국정원에 문의를 했지만, 의미있는 답변은 받지 못했습니다. 특히 친절히 전화까지 해주신 통일부 직원분께 감사드립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1954년과 1961년 사이에 남파/북파 간첩활동을 한 분이 있다면, 그 사실을 정확히 알고 있을 것 같기도 합니다. 그런데 과거 간첩사건을 조사해보니 생각보다 간첩들의 나이가 많아서 지금까지 생존한 분이 계실 가능성은 별로 없어보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;재미있게도 위의 오류를 신고한지 얼마뒤인 2014년 11월 1일에 &apos;&lt;a href=&quot;https://github.com/eggert/tz/commit/bee436b03e28bd5ea984534fcc5e4f3a4d18b47a&quot;&gt;Be less enthusiastic about Shanks and clarify UT vs UTC.&lt;/a&gt; &apos;라는 제목으로 commit이 올라왔습니다. 우리나라 시간대에 대한 잘못된 정보의 출처였던 Shanks의 저서에 많은 오류가 있음을 지적하는 주석이 들어갔습니다. 아시아, 아프리카, 오스트랄라시아, 유럽 등 지역별 정보를 기록하는 모든 파일에 &apos;A good source for time zone historical data outside the U.S. is..&apos;라는 내용이 삭제되고, &apos;unfortunately this book contains many errors and cites no sources.&apos;라는 문장이 추가되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저의 신고가 영향을 준것인지는 알 수 없지만, 이 주석을 기점으로 기존의 데이터를 조금 더 의심하는 계기가 될 것으로 기대합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;비록 오래전 과거데이터라서 지금 시점의 영향성은 적지만, 믿음직한 표준데이터라고 생각했던 IANA Timezone DB에 이렇게 오류가 많았다는 점, 특히 우리나라 관련한 데이터에는 제대로 된 것이 거의 없었다는 사실은 놀랍습니다. 우리나라의 과거 자료와는 별도로, 국제화관련 개발을 하는 사람이라면 내 컴퓨터/내 담당서버에 들어와있는 타임존데이터베이스가 언제 시점인지, 업데이트는 잘 되어 있는지도 잘 확인해봐야겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지의 내용과 관련된 메일스레드는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2014년 10월 30일 00:07:24 (UTC) From 정상혁, To &lt;a href=&quot;mailto:tz@iana.org&quot;&gt;tz@iana.org&lt;/a&gt; : 최초 패치를 보낸 메일 &lt;a href=&quot;http://mm.icann.org/pipermail/tz/2014-October/021830.html&quot; class=&quot;bare&quot;&gt;http://mm.icann.org/pipermail/tz/2014-October/021830.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2014년 10월 30일 06:53:09 (UTC) From Paul Eggert, To 정상혁 : 답장과 1910년 시간대 변경에 대한 문의 &lt;a href=&quot;http://mm.icann.org/pipermail/tz/2014-October/021831.html&quot; class=&quot;bare&quot;&gt;http://mm.icann.org/pipermail/tz/2014-October/021831.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2014년 10월 30일 09:18:57 (UTC) From 정상혁, To Paul Eggert : 1910년의 시간대 변경과 북한시간대에 대한 의견 : &lt;a href=&quot;http://mm.icann.org/pipermail/tz/2014-October/021833.html&quot; class=&quot;bare&quot;&gt;http://mm.icann.org/pipermail/tz/2014-October/021833.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring Loaded로 Tomcat 재시작 없이 수정한 클래스 반영하기</title>
      <link>https://blog.benelog.net//spring-loaded.html</link>
      <pubDate>Mon, 13 Oct 2014 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">spring-loaded.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#다운로드&quot;&gt;다운로드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#tomcat7_maven_plugin으로_실행&quot;&gt;tomcat7-maven-plugin으로 실행&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#1_환경변수_설정&quot;&gt;1. 환경변수 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2_pom_xml에_tomcat7_plugin_추가&quot;&gt;2. pom.xml에 Tomcat7 plugin 추가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3_실행&quot;&gt;3. 실행&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#eclipse_wtp에서_실행&quot;&gt;Eclipse WTP에서 실행&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#1_java_agent_설정&quot;&gt;1. java agent 설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2_기본_reload_설정_제거&quot;&gt;2. 기본 reload 설정 제거&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#main_메서드를_직접_실행하는_경우&quot;&gt;main 메서드를 직접 실행하는 경우&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2014년에 &lt;a href=&quot;https://gist.github.com/benelog&quot;&gt;gist&lt;/a&gt;로 작성했던 글을 2026년에 이 블로그로 옮겼습니다. 발행 날짜는 gist 작성일(2014-10-13)에 맞췄고, 내용도 그 시점 기준입니다. 원본에 있던 Eclipse 설정 화면 스크린샷 3장은 원본 이미지 서버가 없어져 싣지 못했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-loaded&quot;&gt;Spring Loaded&lt;/a&gt;는 개발 환경에서 .java 클래스를 수정했을 때 변경된 클래스 파일만 재로딩하는 도구입니다. Local PC에서 수정과 Tomcat 재시작을 반복하는 시간을 줄여 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 JRebel도 그러하듯이 모든 경우에 완벽한 리로딩이 되지는 않습니다. 메서드 추가·수정은 잘 반영됩니다. 그러나 아래와 같은 경우에는 자동 반영이 되지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;상속 구조의 변경&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reflection 정보가 Cache된 것&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;XML 설정 수정 (프레임워크에 특화된 구현이 들어가지 않으면 당연히 어렵습니다.)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래도 많은 경우 Tomcat 재시작 없이 개발을 이어갈 수 있다면 생산성에 도움이 되리라 생각합니다. 이를 tomcat-maven-plugin과 함께 사용하는 방법을 정리합니다. Linux 명령어를 기준으로 썼지만, 윈도우에서도 같은 역할을 하는 작업을 수행하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;다운로드&quot;&gt;다운로드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참조할 디렉터리에 다운로드합니다. tomcat-maven-plugin을 사용할 예정이면 `pom.xml`이 있는 디렉터리에 받기를 권장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;wget -O springloaded-1.2.1.jar http://search.maven.org/remotecontent?filepath=org/springframework/springloaded/1.2.1.RELEASE/springloaded-1.2.1.RELEASE.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-loaded&quot;&gt;Spring Loaded의 GitHub&lt;/a&gt;에서 최신 버전을 확인한 후 다운로드하는 것이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;tomcat7_maven_plugin으로_실행&quot;&gt;tomcat7-maven-plugin으로 실행&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1_환경변수_설정&quot;&gt;1. 환경변수 설정&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;export MAVEN_OPTS=&quot;-javaagent:springloaded-1.2.1.jar -noverify&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2_pom_xml에_tomcat7_plugin_추가&quot;&gt;2. pom.xml에 Tomcat7 plugin 추가&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;pom.xml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.tomcat.maven&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;tomcat7-maven-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.2&amp;lt;/version&amp;gt;
    &amp;lt;configuration&amp;gt;
        &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
    &amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;3_실행&quot;&gt;3. 실행&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;bash&quot;&gt;mvn tomcat7:run&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jetty나 Tomcat 6을 쓰고 싶다면 &lt;a href=&quot;2879657.html&quot;&gt;Local 개발환경에서 WAS를 띄우는 여러가지 방법&lt;/a&gt;을 참고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;eclipse_wtp에서_실행&quot;&gt;Eclipse WTP에서 실행&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1_java_agent_설정&quot;&gt;1. java agent 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eclipse 메뉴의 Run &amp;gt; Run Configurations에서 Tomcat 실행 설정을 찾은 다음, Arguments 탭의 VM arguments란에 `-javaagent`를 추가합니다. 한 번이라도 Tomcat을 WTP로 실행해야 해당 설정이 생깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;-javaagent:/다운로드경로/springloaded-1.2.1.jar -noverify&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2_기본_reload_설정_제거&quot;&gt;2. 기본 reload 설정 제거&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Servers 뷰에서 Tomcat을 선택하고 Overview 탭의 Server Options란에서 &apos;Modules auto reload by default&amp;#8217;를 선택 해제합니다. Modules 탭의 해당 모듈 설정에서도 Auto reloading이 해제되어 있는지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;main_메서드를_직접_실행하는_경우&quot;&gt;main 메서드를 직접 실행하는 경우&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Boot나 Embedded Tomcat, Jetty를 이용해서 main 메서드로 직접 WAS를 띄우는 경우도 있습니다. 이때도 Run &amp;gt; Run Configurations의 Arguments 탭에서 VM arguments란에 같은 &lt;code&gt;-javaagent&lt;/code&gt; 설정을 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-loaded&quot;&gt;Spring Loaded (spring-projects/spring-loaded)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;2879657.html&quot;&gt;Local 개발환경에서 WAS를 띄우는 여러가지 방법&lt;/a&gt; — 이 블로그의 내장 WAS 활용 글&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>통합 테스트를 위한 내장 Tomcat, tomcat-bed</title>
      <link>https://blog.benelog.net//tomcat-bed.html</link>
      <pubDate>Sat, 29 Jun 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">tomcat-bed.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#왜_만들었나&quot;&gt;왜 만들었나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#핵심_클래스_webapplicationserver&quot;&gt;핵심 클래스 WebApplicationServer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#spring_테스트와_통합&quot;&gt;Spring 테스트와 통합&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jwebunit으로_화면_검증&quot;&gt;JWebUnit으로 화면 검증&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#한계&quot;&gt;한계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;admonitionblock note&quot;&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class=&quot;icon&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Note&lt;/div&gt;
&lt;/td&gt;
&lt;td class=&quot;content&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 글은 2026년에 예전 예제 저장소들을 정리하면서 작성했습니다. &lt;code&gt;benelog/tomcat-bed&lt;/code&gt; 저장소를 이 블로그 저장소의 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed&quot;&gt;examples/tomcat-bed&lt;/a&gt;로 병합했고, 발행 날짜는 원본 저장소의 마지막 커밋 날짜(2013-06-29)에 맞췄습니다. 본문도 그 시점의 기술 기준으로 서술했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;tomcat-bed는 통합 테스트 안에서 내장 Tomcat을 띄우는 작은 유틸리티입니다. Spring 테스트 컨텍스트에 bean으로 등록하면 테스트가 시작될 때 WAS를 올리고 끝나면 내립니다. 별도의 배포 작업 없이 JSP 렌더링 결과까지 검증하는 UI 테스트를 JUnit 안에서 실행하려고 만들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;왜_만들었나&quot;&gt;왜 만들었나&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;웹 계층을 검증하는 방식은 크게 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;col style=&quot;width: 40%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;방식&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;장점&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;한계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;배포된 서버 대상 UI 테스트&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실제 운영과 같은 환경을 검증&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;테스트 전에 배포 작업이 필요하고, Test Coverage 측정이 어려움&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Spring MVC Test (&lt;code&gt;MockMvc&lt;/code&gt;)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;서블릿 컨테이너 없이 빠르게 실행&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Controller가 반환한 view 이름까지만 검증. JSP가 실제로 어떻게 렌더링되는지는 알 수 없음&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;내장 WAS + UI 테스트 (tomcat-bed)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;배포 없이 JSP 렌더링까지 검증. EclEmma로 Eclipse에서 바로 커버리지 확인 가능&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;실제 Tomcat이 시작되므로 `MockMvc`보다 느림&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring 3.2에 포함된 Spring MVC Test는 서블릿 컨테이너 없이 &lt;code&gt;DispatcherServlet&lt;/code&gt; 계층을 검증하는 좋은 도구입니다. 다만 JSP 렌더링 결과는 검증 범위 밖입니다. 화면까지 확인하려면 실제 서블릿 컨테이너가 필요한데, 매번 배포해서 테스트하기는 번거롭습니다. 그래서 테스트 프로세스 안에서 Tomcat을 직접 띄우는 방식을 시도했습니다. &lt;a href=&quot;2879657.html&quot;&gt;Local 개발환경에서 WAS를 띄우는 여러가지 방법&lt;/a&gt;에서 다뤘던 내장 WAS 활용법을 테스트로 가져온 셈입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;핵심_클래스_webapplicationserver&quot;&gt;핵심 클래스 WebApplicationServer&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;핵심은 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed/tomcat-bed-test/src/test/java/net/benelog/tomcatbed/WebApplicationServer.java&quot;&gt;WebApplicationServer.java&lt;/a&gt; 클래스 하나입니다. Tomcat 6의 &lt;code&gt;org.apache.catalina.startup.Embedded&lt;/code&gt; API로 Engine, Host, Context를 조립합니다. 주요 부분만 옮기면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;WebApplicationServer.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public WebApplicationServer(String contextPath, int port, String appBase) {
	this.port = port;
	this.container = new Embedded();
	this.container.setName(&quot;TomcatEmbeddedServer&quot;);
	this.appContext = createAppContext(contextPath);
	Host localHost = createHost(appBase, appContext);
	Engine engine = createEngine(localHost);
	this.container.addEngine(engine);

	Connector connector = this.container.createConnector(
			localHost.getName(), port, false);
	this.container.addConnector(connector);
}

@PostConstruct
public void start() throws IOException, LifecycleException {
	log.info(&quot;Starting Tomcat... (port={})&quot;, port);

	if (isEmptyPort()) {
		this.container.start();
		this.running = true;
		this.servletContext = appContext.getServletContext();
	} else {
		log.info(&quot;The port({}) is already occupied.&quot;, port);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설계 포인트는 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;배포 없는 실행&lt;/strong&gt; : `appBase`를 `src/main`으로 지정하면 war를 만들지 않고 소스 트리의 webapp 폴더를 그대로 서비스합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;포트 점유 확인&lt;/strong&gt; : 지정한 포트가 이미 사용 중이면 내장 Tomcat을 시작하지 않습니다. PC에 개발용 WAS가 떠 있으면 그 서버를 대상으로 같은 테스트가 그대로 동작합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;수명 주기 관리&lt;/strong&gt; : &lt;code&gt;@PostConstruct&lt;/code&gt;/`@PreDestroy`로 시작과 종료를 선언해서, Spring 컨테이너가 bean의 수명에 맞춰 WAS를 올리고 내립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spring_테스트와_통합&quot;&gt;Spring 테스트와 통합&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`WebApplicationServer`를 테스트용 컨텍스트에 bean으로 등록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;applicationContext-ui-test.xml&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;util:properties id=&quot;testServer&quot; location=&quot;classpath:testServer.properties&quot; /&amp;gt;

&amp;lt;bean id=&quot;was&quot; class=&quot;net.benelog.tomcatbed.WebApplicationServer&quot;
	c:contextPath=&quot;#{testServer.contextPath}&quot;
	c:port=&quot;#{testServer.port}&quot;
	c:appBase=&quot;src/main&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring TestContext 프레임워크는 같은 설정을 쓰는 테스트끼리 `ApplicationContext`를 캐싱해서 공유합니다. 덕분에 여러 테스트 클래스를 함께 실행해도 WAS는 한 번만 시작됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버 접속 정보는 별도 속성 파일로 분리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;testServer.properties&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;properties&quot;&gt;contextPath=/
port=8080
baseUrl=http://localhost:8080&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`baseUrl`을 개발 서버나 Staging 서버 주소로 바꾸면 같은 테스트를 다른 환경을 대상으로도 실행할 수 있습니다. 그 경우 해당 포트가 이미 사용 중이 아니므로 내장 Tomcat만 로컬에서 뜨지 않을 뿐, 테스트 코드는 동일합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jwebunit으로_화면_검증&quot;&gt;JWebUnit으로 화면 검증&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트에서는 JWebUnit으로 실제 렌더링된 화면을 검증합니다. &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed/tomcat-bed-test/src/test/java/net/benelog/tomcatbed/test/ZipCodePageTest.java&quot;&gt;ZipCodePageTest.java&lt;/a&gt;는 폼 입력과 전송, 응답 화면 내용까지 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ZipCodePageTest.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { &quot;/applicationContext-ui-test.xml&quot;})
public class ZipCodePageTest {
	@Value(&quot;#{testServer.baseUrl}&quot;)
	String baseUrl;

	@Before
	public void prepare() {
		setBaseUrl(baseUrl);
		setScriptingEnabled(false);
	}

	@Test
	public void zipCodeForm() {
		beginAt(&quot;/zipCodeForm&quot;);
		assertFormPresent();
		assertSubmitButtonPresent();

		setTextField(&quot;zipCode&quot;, &quot;121-270&quot;);
		submit();

		assertResponseCode(HttpStatus.OK.value());
		assertTitleEquals(&quot;Address&quot;);
		assertMatch(&quot;서울특별시 마포구 상암동&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;`@ContextConfiguration`으로 위의 테스트 컨텍스트를 지정하기만 하면, 테스트가 실행될 때 WAS가 준비됩니다. JSP가 렌더링한 제목과 본문 텍스트를 그대로 단언할 수 있다는 점이 `MockMvc`와의 차이입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 예제 프로젝트에 두 방식을 나란히 두었습니다. &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed/tomcat-bed-test/src/test/java/net/benelog/tomcatbed/test/HomeMvcTest.java&quot;&gt;HomeMvcTest.java&lt;/a&gt;는 Spring MVC Test로 Controller 계층을, &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed/tomcat-bed-test/src/test/java/net/benelog/tomcatbed/test/HomePageTest.java&quot;&gt;HomePageTest.java&lt;/a&gt;는 내장 Tomcat 위에서 같은 화면을 검증합니다. 빠른 피드백은 `MockMvc`로, 화면까지 포함한 검증은 tomcat-bed로 나눠 쓰는 구성입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;한계&quot;&gt;한계&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;실제 Tomcat이 시작되므로 Spring MVC Test보다 시작 속도가 느립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;태그 라이브러리 선언 파일(&lt;code&gt;*.tld&lt;/code&gt;)을 수동으로 등록해야 인식됩니다. 예제에서도 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed/tomcat-bed-test/src/main/webapp/WEB-INF/tld&quot;&gt;WEB-INF/tld&lt;/a&gt;에 JSTL과 Spring 태그의 tld 파일을 직접 넣었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tomcat 6의 &lt;code&gt;Embedded&lt;/code&gt; API를 사용했는데, Tomcat 7부터는 더 간결한 &lt;code&gt;org.apache.catalina.startup.Tomcat&lt;/code&gt; 클래스가 이 역할을 대신합니다. 새로 시작한다면 그쪽이 출발점으로 적합합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/tomcat-bed&quot;&gt;예제 저장소 (examples/tomcat-bed)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://stackoverflow.com/questions/640022/howto-embed-tomcat-6&quot;&gt;Howto embed Tomcat 6? (Stack Overflow)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://stackoverflow.com/questions/6349472/embedded-tomcat-not-serving-static-content&quot;&gt;Embedded tomcat not serving static content (Stack Overflow)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;2879657.html&quot;&gt;Local 개발환경에서 WAS를 띄우는 여러가지 방법&lt;/a&gt; — 이 블로그의 내장 WAS 활용 글&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;2875999.html&quot;&gt;Winstone, 경량 Servlet container&lt;/a&gt; — 또 다른 경량 컨테이너 소개 글&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 Claude Code와 정상혁이 함께 작성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Android에서 @Inject, @Test</title>
      <link>https://blog.benelog.net//3017438.html</link>
      <pubDate>Wed, 17 Apr 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3017438.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 지난달에 썼던 글이 네이버의 기술블로그인 &lt;a href=&quot;http://helloworld.naver.com/&quot;&gt;http://helloworld.naver.com&lt;/a&gt;에 공개되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://helloworld.naver.com/helloworld/342818&quot;&gt;Android에서 @Inject, @Test&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Android에서 DI + Test 스타일로 개발을 하는데 겪는 어려움과 이를 극복하는데 도움을 주는 Android annotations와 Robolectric 등의 오픈소스 프레임워크를 비교하고 소개했습니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>10 things you need to know about Android and Google Play - Tony Chan (Google), GDG Korea Android 컨퍼런</title>
      <link>https://blog.benelog.net//3017439.html</link>
      <pubDate>Wed, 17 Apr 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3017439.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 2013/04/13일, 종로 페럼타워에서 열렸던 &apos;구글 개발자와 함께하는 GDG Korea Android 컨퍼런스 &apos;에서 공유된 내용입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참석해서 메모했던 내용을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구글 Play와 API, 권장 UI스타일, 정책 등 다양한 주제를 이야기한 발표였습니다.  UX설계를 할 때 많은 고민이 필요하겠다고 느꼈습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;1_think_global&quot;&gt;1. Think Global&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;한 지역만 염두에 둔다면 기회를 놓치고 있는것이다. 구글 Play 수익의 67%가 미국외에서 나오고 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Localization을 고려해라. 구글 Play의 그래픽스와 비디오도 이제 로컬라이즈되었다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;웹애플리케이션은 국제화에 대한 고려를 하는 비율이 높지 않습니다. 앱개발은 반대로 국제화를 고려하지 않는 것이 더 합당한 이유가 있어야하는 듯합니다. 기준이 차이가 생겼다고 느껴집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;2_pure_android&quot;&gt;2. Pure Android&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;다른 플랫폼의 UI형태를 모방하지 마라. 각각의 플랫폼은 고유한 스타일이 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;플랫폼 특화된 아이콘을 밖에서 쓰지 말라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;바닥에 붙은 탭바를 쓰지 마라. (아이폰 스타일)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;별도의 라벨이 붙은 백버튼, &apos;right point caret&apos;(&amp;gt;) 버튼을 쓰지 마라. 백버튼을 플랫폼에서 제공하는 것을 쓰도록 유도해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UI에 대한 지침을 지키지 않으면 사용자가 안 좋은 Rating을 할 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이전의 안드로이드 버전에 지원하던 스타일의 레가시 버튼을 쓰지 마라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Action bar를 써라. 곧 support 라이브러리를 통해 하위 버전에서도 ActionBar를 쓸 수 있게 될 것이다. 지금도 오픈소스로 &lt;a href=&quot;http://actionbarsherlock.com/&quot;&gt;Actionbar-sherlock&lt;/a&gt;이 있지만, 기본 SDK에서 이제 지원된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;가장 최신의 SDK에 targeting하라. 최신의 SDK에서만 돌아가야한다는 의미는 아니다. 하위버전에서도 돌아가도록 만들면서도 상위버전에서는 최신의 UI 요소들을 활용해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개별 앱의 UI는 같은 플랫폼 내에서의 일관성을 해치지 않아야한다는 지침을 강조했습니다. 같은 사용자가 2개의 다른 플랫폼을 오가면서 같은 앱을 사용하는 경우보다는, 하나의 플랫폼에서 여러 앱을 사용하므로 플랫폼 내에서의 일관성이 중요한 것은 당연한듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기획자나 디자이너 입장에서는 하나의 앱을 출시하면 여러 플랫폼이 동일한 UI로 나오기를 바랄 수도 있을 듯한데, 이러한 지침을 초기부터 염두에 두고 엔지니어가 피드백을 줄 수 있어야겠습니다. &lt;a href=&quot;http://developer.android.com/design/index.html&quot;&gt;안드로이드 디자인가이드&lt;/a&gt;도 정독해봐야겠구나 싶었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 &lt;a href=&quot;http://actionbarsherlock.com/&quot;&gt;Actionbar-sherlock&lt;/a&gt;에서 해주던 ActionBar의 하위 호환성 지원이 support 라이브러리에 들어간다는 소식도 반가웠습니다. Fragment가 support 라이브러리에서 지원되는 것을 생각하면 자연스러운 개선이기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;3_design_for_all_form_factors&quot;&gt;3. Design for all form-factors&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Fragment를 써라&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;타블릿과 폰을 위해서 별도의 프로젝트를 프로젝트를 만드는것은 바람직하지 않다. Fragment를 이용해서 타블릿에서는 하나의 Activity에 여러개의 Fragment가, 폰에서는 Activity당 하나의 Fragment를 보여주는 식으로 구성하면 얼마든지 하나의 프로젝트로 구성할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;UI요소의 크기는 기기의 해상도에 독립적이도록 DP를 쓰고, 텍스트에는 SP를 써라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;적어도 3개의 레이아웃을 고려해라. 폰, 7인치 타블릿, 10인치 타블릿. DP는 해상도 문제만 해결할 뿐, 다른 사이즈에 대한 문제는 별도로 고려해야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;지원하려는 화면 밀도(Density), 해당도에 맞는 Asset, 이미지를 각각 준비해라. Aliasing은 보기에도 좋지 않고 CPU를 많이 사용해서 배터리 사용시간을 짧아지게 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DP에 대해서는 아래 그림 1장이 직관적인 이해에 많은 도움이 되었습니다.
&lt;span class=&quot;image-wrap&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://cfile8.uf.tistory.com/image/11355F45516932300FB576&quot; alt=&quot;11355F45516932300FB576&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이미지 출처 : &lt;a href=&quot;http://www.kmshack.kr/317#.UWy9VIpdUbj&quot; class=&quot;bare&quot;&gt;http://www.kmshack.kr/317#.UWy9VIpdUbj&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;웹에서는 &apos;방탄형 웹&amp;#8217;이나 &apos;반응형 웹디자인&amp;#8217;에 대한 많은 노하우가 공유되고 있는데, 안드로이드에서도 점점 더 섬세한 노하우들이 공유될 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;4_correct_back_button_behaviors&quot;&gt;4. Correct back button behaviors&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;앱의 메인 화면에서 사용자는 홈스크린으로 바로 돌아갈 수 있어야 한다. 종료 할때 다이얼로그로 종료할지 물어보는 방식으로 Back에 대한 이벤트를 강제적으로 막지마라.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많은 앱이 이렇게 되어 있지 않기에 다소 의외의 가이드라고 느껴졌습니다. 이것도 일관성을 생각하면 의미 있는 지침입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;5_proper_notification&quot;&gt;5. Proper notification&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;핵심 기능에 대해서 통지해라. 광고에 Notification을 쓰지 마라.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;6_app_quality_improvement&quot;&gt;6. App quality improvement&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://bit.ly/google-play-core-checklist&quot; class=&quot;bare&quot;&gt;http://bit.ly/google-play-core-checklist&lt;/a&gt; 참고&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://bit.ly/google-play-tablet-checklist&quot; class=&quot;bare&quot;&gt;http://bit.ly/google-play-tablet-checklist&lt;/a&gt; 참고&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한글 번역도 &lt;a href=&quot;http://googledevkr.blogspot.kr/2013/01/app-quality-guidelines.html&quot; class=&quot;bare&quot;&gt;http://googledevkr.blogspot.kr/2013/01/app-quality-guidelines.html&lt;/a&gt; 에서 보실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이 포스트를 쓰고 난 후에 권순선님께서 알려주셨습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;7_permission&quot;&gt;7. Permission&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;핵심기능에 필요한 최소한의 권한만 부여하도록 해라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;민감한 권한을 너무 많이 가지고 있으면 당신에게 해가 된다. 앱의 다운로드 숫자나 주목 받을 기회가 줄어들 것이다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;8_google_play_service&quot;&gt;8. Google play service&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;구글에서 제공하는 서비스 API를 나열하면서 소개.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Maps API 2, Google+, Google Authorization, Google Play services APK 등&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 내용은 &lt;a href=&quot;http://developer.android.com/google/play-services/index.html에&quot; class=&quot;bare&quot;&gt;http://developer.android.com/google/play-services/index.html에&lt;/a&gt; 자세히 나와 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;9_new_platform_api&quot;&gt;9. New platform API&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;새로 추가된 API를 간단히 나열.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Lock screen widget&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Seconary display&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Day dreams (interactive screensaver mode)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Natvie RTL(right-to-left) support&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tablet sharing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;최근 업데이트된 API 소개&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GCM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Analystic SDK&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In-app Billing V3 (V2는 더이상 쓰지마라)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Youtube android player&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자세한 내용은 &lt;a href=&quot;http://developer.android.com/about/versions/jelly-bean.html에&quot; class=&quot;bare&quot;&gt;http://developer.android.com/about/versions/jelly-bean.html에&lt;/a&gt; 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;발표된지는 한참된 기능이지만, 이렇게 모아서 보니 앱개발자가 화면을 다양하게 활용할 수 있는 여지가 더 늘어나고 있다고 느껴졌습니다. 이에 따라 새로운 사업모델도 나올 수 있었는데, 이런 아이디어들이 기획부서에서만 나오기를 기다리는 것보다 개발자들도 적극 제안을 해 보면 어떨까하는 생각이 들었습니다. 개발자들은 새로운 API가 추가되는것을 늘 관심있게 보고 있기 때문에 자연스럽게 아이디어가 나올만도 합니다. Lock screen widget을 이용한 광고 제품이 나왔듯이, 앞으로도 그런 기회가 많이 남아 있을 것이라고 기대가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;10_publishing_on_google_play&quot;&gt;10. Publishing on google play&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Google play는 풍부하게 미디어를 활용한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;특징을 멋지고 깔끔한 이미지로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스크린샷&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;짧은 유튜브 동영상&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;로컬라이제이션&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;대표적인 정책 위반 사례들.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;3rd party 지불&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;다운로드를 위해 3rd party 사이트로의 연결&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스팸 키워드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;별점에 인센티브 부과&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;정책에 대한 자세한 내용은 &lt;a href=&quot;http://bit.ly/google-play-policy-edu&quot; class=&quot;bare&quot;&gt;http://bit.ly/google-play-policy-edu&lt;/a&gt; 참조&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정책에 따라 구현이 달라져야 할 부분을 미리 파악하고 있으려면 꾸준한 관심을 가져야겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Android App과 TDD - 임유진 (Kakao), GDG Korea Android 컨퍼런스 중에서</title>
      <link>https://blog.benelog.net//3017442.html</link>
      <pubDate>Wed, 17 Apr 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3017442.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 2013/04/13일, 종로 페럼타워에서 열렸던 &apos;구글 개발자와 함께하는 GDG Korea Android 컨퍼런스 &apos;에서 공유된 내용입니다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;발표_자료&quot;&gt;발표 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;발표 자료 : &lt;a href=&quot;http://goo.gl/1WyAz&quot; class=&quot;bare&quot;&gt;http://goo.gl/1WyAz&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예제코드 : &lt;a href=&quot;https://github.com/eugenelgm/AndroidForTest&quot; class=&quot;bare&quot;&gt;https://github.com/eugenelgm/AndroidForTest&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;요약&quot;&gt;요약&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 핵심으로 기억하는 내용은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;앱이 기능이 점점 추가되고 복잡해져 가면서 코드가 점점 누더기가 되어갔다. 수정에 대한 부작용을 파악하기 어려워져 갔다. 그래서 테스트의 필요성이 느껴졌다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Android의 기본 Test Framework는 너무 느려서 포기했다. 한번 실행에 30~40초가 걸렸다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Robolectric도 검토했으나, 없는 기능이 많고 라이브러리 충돌 때문에 포기했다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;테스트가 실패할 때 Robolectric의 버그인지 앱의 버그인지 확인하기 어려워서 디버깅에 시간이 걸렸다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;암호화 관련 라이브러리에서 충돌이 일어남&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;결국 UI쪽의 테스트보다는 UI를 벗어난 layer에서 핵심로직을 테스트하는데 집중했다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;테스트 기법&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;android.util.Log를 호출하는 부분은 별도의 Wrapping 클래스로 작성. 테스트 프로젝트에서는 같은 패키지에 같은 클래스 이름으로 System.out으로 로그를 출력하는 클래스를 작성. class loader의 순서를 조정해서 테스트 코드에서는 테스트용 Logger클래스를 호출.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;실환경용 Log 랩퍼 클래스 : &lt;a href=&quot;https://github.com/eugenelgm/AndroidForTest/blob/master/src/com/example/test/util/Logger.java&quot;&gt;Logger.java
&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;테스트용 Log 클래스  : &lt;a href=&quot;https://github.com/eugenelgm/AndroidForTest/blob/master/tests/src/com/example/test/util/Logger.java&quot;&gt;Logger.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Background Thread에 대한 테스트도 래핑 클래스를 이용. 테스트 환경에서는 테스트 코드와 같은 스레드에서 동기적으로 실행되도록 Runnable.run을 호출하는 래퍼클래스를 호출함.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;실환경용 스레드 실행 클래스 : &lt;a href=&quot;https://github.com/eugenelgm/AndroidForTest/blob/master/src/com/example/test/util/DatabaseJobQueue.java&quot;&gt;DatabaseJobQueue.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;테스트용 Log 클래스  : &lt;a href=&quot;https://github.com/eugenelgm/AndroidForTest/blob/master/tests/src/com/example/test/util/DatabaseJobQueue.java&quot;&gt;DatabaseJobQueue.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;같은 interface를 구현한 테스트용 Mock객체를 작성하고, 주입은 별도의 setter 메서드를 사용.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API에 대한 테스트는 정적파일을 통해서 함. 후임자가 API의 명세를 예시로 금방 확인할 수 있는 장점도 생김&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Context에 대한 참조 등 Android에 대한 의존성을 제거하기 어려운 부분은 PowerMock + Mockito로 해결&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;PowerMock + Mockto로 Context.getString(int)의 동작을 가로채는 예제 : &lt;a href=&quot;https://github.com/eugenelgm/AndroidForTest/blob/master/tests/test/com/example/test/MainActivityTest.java&quot;&gt;MainActivityTest.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;테스트할 수 있는 Layer를 구분하다 보니 설계 개선을 이끔&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TDD에 대한 오해&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;꼭 Dalvik에서 테스트해야 의미가 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TDD로 모든 에러를 잡을 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;개발 후에 만들어도 된다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;질문&quot;&gt;질문&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;좀 더 자세히 알고 싶은 점이 있어서 발표가 끝난 후에 아래 2가지를 질문했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1_robolectric의_라이브러리_충돌의_구체적인_사례&quot;&gt;1. Robolectric의 라이브러리 충돌의 구체적인 사례&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 정리한 암호화 라이브러리와의 충돌사례를 알려주셨고, Robolectric의 버그 때문에 디버깅이 어려웠다는 이야기도 구체적으로 해주셨습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2_depenency_injection_프레임워크를_고려&quot;&gt;2. Depenency Injection 프레임워크를 고려&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;검토는 했지만 쓰지는 않았고, 발표한 내용은 사례일 뿐이기 때문에 각자 생각하는 좋은 방법이 있으면 계속 시도해봤으면 좋겠다고 말씀해주셨습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;의견&quot;&gt;의견&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;추가로 제 의견을 덧붙이면, Roblectric에 빈틈이 많다는 단점은 저도 공감은 가지만 꼭 UI를 테스트하지 않더라도 Robolectric을 부분적으로 유용하게 쓸 수는 있다고 생각합니다. 예를 들면 android.util.Log 클래스를 쓰는 코드도 Robolectric을 쓰면 별도의 랩퍼 클래스가 없어도 편하게 테스트할 수 있습니다. ShadowLog라는 클래스를 사용하면  Console이나 특정 파일등 로그를 쓰는 위치도 좀 더 편하게 조정할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;ShadowLog.stream = System.out;
Robolectric.bindShadowClass(ShadowLog.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 멀티스레드에 대한 테스트를 할 때도 Robolectric의 RobolectricBackgroundExecutorService를 쓰면 편할 때가 있습니다. 이 클래스도 다른 스레드를 생성하지 않고 호출한 쪽과 같은 스레드에서 Runnable 클래스를 실행해 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Android-annotations를 쓰면 @Background가 붙은 메서드는 BackgroundExecutor라는 클래스를 통해서 실행되는데, 이 클래스에 있는 executor라는 멤버변수를 교체하면 스레드의 생성 정책을 조절 할 수 있습니다. 따라서 테스트를 할 때는 아래와 같이 테스트용 Executor를 넣으면 자연스럽게 같은 스레드에서 동기적으로 Runnable 클래스를 실행할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;BackgroundExecutor.setExecutor(new RobolectricBackgroundExecutorService());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 구조적으로 Thread의 Executor를 바꿔치기 하기 힘든 경우에는 &lt;a href=&quot;https://code.google.com/p/awaitility/&quot;&gt;Awaitility&lt;/a&gt;라는 라이브러리도 사용해볼만도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 SDK버전에 따라서 다르게 돌아가는 코드가 있다면 Robolectric에서 아래와 같이 조작을 할 수 있습니다. (물론 PowerMock을 써도 같은 일을 할 수 있기는 합니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Robolectric.Reflection.setFinalStaticField(Build.VERSION.class, &quot;SDK_INT&quot;, Build.VERSION_CODES.JELLY_BEAN);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;암튼 Robolectric에 빈틈이 많기에 큰 기대를 하지 말고 UI 레이어의 테스트에는 많은 욕심을 부리지 말자고 생각하지만 그래도 몇가지 매력적인 기능이 있어서 Robolectric을 아예 외면을 할 수는 없었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mockito + Powermock의 조합은 강력하지만, 구조를 고치기 어려운 레가시 코드에만 한정해서 썼으면 한다는 의견입니다. 가능하다면 Powermock이 없어도 테스트할 수 있도록 구조를 개선하는 것이 더 클래스의 역할이 명확해지고, 앞으로 기능을 추가하거나 읽기에도 좋은 코드가 되기 때문입니다. 그렇게 구조개선을 하는데는 DI 프레임워크가 많은 도움이 되기도 합니다.  DI 프레임워크를 쓰면 Context에 대한 직접 의존이나 안드로이드 기본 프레임워크의 final 메서드의 동작을 가로채야할 일이 적어져서  훨씬 테스트하기 편해집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Helloworld에 올라온  &lt;a href=&quot;http://helloworld.naver.com/helloworld/342818&quot;&gt;Android에서 @Inject, @Test&lt;/a&gt; 에서 이에 대해 자세히 적었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;소감&quot;&gt;소감&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개인적으로 많은 고민을 했던 주제였고, 발표자께서 내리신 결론이 저와 거의 비슷했기에 무척 반가웠습니다. 저도 Android의 기본 테스트 클래스를 쓰면서 느꼈던 좌절감에 결국 JVM에서 테스트를 해야 TDD로서 의미가 있다고 느꼈습니다. UI에 대한 테스트보다는 안드로이드와 독립적인 Layer를 테스트하는 것이 ROI가 높고, 좋은 설계를 이끈다는 점도 공감이 갔습니다. 로그호출 부분이나 멀티스레드에 대한 테스트 등 제가 했던 고민도 보편적인 문제였다는 것도 확인했습니다. API의 호출결과를 정적 파일로 저장해두고 테스트 코드에서 파싱부터 검증하는 기법은 저도 Server to Server API클라이언트 모듈 테스트 때 많이 썼던 방법이였습니다&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 편향된 생각을 가졌을지 늘 걱정이 되었는데, 같은 의견을 가지신 분이 구체적인 사례까지 공유해주셔서 많은 도움이 되었습니다.  앞으로  다른 분께도 안드로이드에서 TDD를 자신있게 권장해드릴 용기를 얻었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring tag library +  Tomcat7.x 또는 Glassfish 2.2.x의 EL Injection 위험</title>
      <link>https://blog.benelog.net//3007743.html</link>
      <pubDate>Tue, 5 Mar 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3007743.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;취약점의_조건&quot;&gt;취약점의 조건&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 조건을 모두 충족시킨다면 코드는 치명적인 Remote code execution 취약성이 존재할 여지가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jcp.org/aboutJava/communityprocess/mrel/jsr245/index.html&quot;&gt;EL 2.2&lt;/a&gt;를 지원하는 서블릿 컨테이너를 쓰거나 EL 2.2 라이브러리를 직접 jar파일로 참조해서 쓰고 있다. (대표적으로 Tomcat 7.x혹은 Glassfish 2.2.x)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring 3.1.x 미만 버전을 쓰고 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring의 JSP Tag( &amp;lt;spring:message.. 등)을 쓰고 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring의 JSP Tag에서 EL을 지원하는 속성에 사용자가 입력한 값이 들어갈 수 있다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;각 태그의 속성의 EL지원 여부는 &lt;a href=&quot;https://docs.google.com/document/d/1dc1xxO8UMFaGLOwgkykYdghGWm_2Gn0iCrxFsympqcE/edit&quot;&gt;Stefano Di Paola 등의 Expression Language Injection 보고서&lt;/a&gt; 참조&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위에서 첫번째 조건이 해당하지 않더라도 다른 취약점을 조심해야 합니다. EL 2.2을 사용하지 않더라도 JSP 2.0이상을 지원하는 서블릿 컨테이너를 쓰면서 2,3,4에 해당하는 코드라면 애플리케이션의 중요한 정보를 노출시킬 수 있는 취약점이 존재할 위험이 있습니다. Remote code execution만큼 심각하지는 않지만, 역시나 조치가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 취약 지점은 의도하지 않은 정보를 노출할 수 있는 위험성으로 이미 2011년 9월 지적되어 보안되었으나, 이번달인 2013년 1월에는 같은 취약 지점을 통해서 원격 코드를 실행하는 기법이 발견되어 위험성 등급이 올라갔습니다. 즉, 해당 라이브러리가 취약성을 유발한다는 점 자체는 새로운 것이 아니고, 이미 이를 보강하는 버전이 나와 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 취약점의 원인과 조치방법은 아래 링크에 정리해놓았습니다. (코드가 들어간 글은 GIST를 쓰는 것이 훨씬 편하네요.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/benelog/4582041&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/4582041&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Markdown + Dropbox</title>
      <link>https://blog.benelog.net//3007746.html</link>
      <pubDate>Tue, 5 Mar 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">3007746.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;업데이트&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2019.04.10 : 현시점에서는 Calepin 서비스도 종료되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2013.03.05 : GIST에 썼던 글을 옮기어 봅니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Springnote 서비스 종료 전에 쓴 글이라서  Springnote의 마이그레이션 코드는 이제는 쓸수가 없네요)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;자료를_정리하는_형식과_도구에_대한_고민&quot;&gt;자료를 정리하는 형식과 도구에 대한 고민&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;회사에서 만든 자료들은 MOSS(Microsoft Office SharePoint Server)나 위키, 메일함 등이 있으니 큰 아쉬움은 없는데, 개인적인 자료들은 어떻게 정리하고 보관, 공유할지가 늘 고민이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 공개하고 싶지 않은 것들은 &lt;a href=&quot;http://www.evernote.com&quot;&gt;에버노트&lt;/a&gt;와 &lt;a href=&quot;http://www.dropbox.com&quot;&gt;드롭박스&lt;/a&gt;를 쓰고, 공유할 자료들은 스프링노트와 &lt;a href=&quot;http://blog.benelog.net&quot;&gt;이글루스 블로그&lt;/a&gt;을 활용하고 있습니다.코드들은 주로 &lt;a href=&quot;https://github.com/benelog&quot;&gt;Github&lt;/a&gt;와 &lt;a href=&quot;https://gist.github.com/benelog&quot;&gt;Gist&lt;/a&gt;에 올립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 다양한 서비스를 쓰다보니 자료를 서비스 간에 이동을 할 일이 생기면 많이 불편합니다. 예를 들면 에버노트에 있는 자료를 블로그에 올린다거나 스프링노트에 있는 글을 구글사이트로 옮길 때가 그렇습니다.그냥 한 두 페이지면 복사해서 붙여넣기를 하겠지만, 여러 페이지를 옮길 때 그렇게 하면 단순 반복 작업에 들어가는 시간이 아깝습니다.웹 브라이저나 웹 에디터의 차이 때문에 칸 맞추기 같은 사소한 편집에 시간을 많이 허비하기도 합니다.웹이나 클라이언트 환경 등 다양한 환경에서 모두 편집이 간편했으면 하는 바램도 있었습니다. 우분투를 쓰는 시간이 많다보니 MS워드처럼 특정 OS가 종속적인 편집기는 별로 좋아하지 않기도 합니다. 그리고 정리한 내용을 자주 찾아보니 검색과 탐색 기능을 쓰기 쉬웠으면도 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이번에 스프링노트가 종료될 예정이라 어떤 형식으로 자료를 정리할 것인지에 대한 고민이 더 커졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;요구사항을 정리하면&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;자료를 이동하는 비용이 적어야하고 (이식성)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;자료의 모양 때문에 편집을 하는 시간이 되도록 없었으면 하고&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;특정 환경에 종속적이지 않은 다양한 문서 편집기를 사용할 수 있어야 하고&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;검색 기능을 간편하게 쓸 수 있어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 점들을 고민하다보니 우선 정리하는 자료의 형식은 마크다운을 선택하게 되었습니다. Github와 Gist를 쓰면서 그 단순함에 만족을 했고, &lt;a href=&quot;http://tumblr.com&quot;&gt;텀블러&lt;/a&gt;, &lt;a href=&quot;https://trello.com/&quot;&gt;트렐로&lt;/a&gt;등 지원하고 있는 서비스도 늘어가고 있어서 이식성도 높다고 생각했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 원본 데이터를 파일단위로 관리하고 싶었습니다. 쓰던 서비스가 망하더라도 파일을 단순히 복사해서 다른 서비스로 옮길 수 있다면 이전 비용이 적을 것이기 때문이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Github를 이용한 &lt;a href=&quot;http://octopress.org/&quot;&gt;Octopress&lt;/a&gt;도 검토했으나, 버전관리까지는 필요없이 최종 파일만 관리하면 되었기 때문에 Dropbox를 썼으면 좋겠다고 생각했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;사람 생각은 다 비슷한지, 이미 Dropbox + Markdown을 이용한 블로그와 위키 서비스가 나와 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;블로그 서비스 : &lt;a href=&quot;http://calepin.co&quot; class=&quot;bare&quot;&gt;http://calepin.co&lt;/a&gt;, &lt;a href=&quot;http://scriptogr.am/&quot; class=&quot;bare&quot;&gt;http://scriptogr.am/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;위키 서비스 : &lt;a href=&quot;http://wikipackit.com&quot; class=&quot;bare&quot;&gt;http://wikipackit.com&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;calepin_markdown_dropbox로_블로그_만들기&quot;&gt;Calepin( =Markdown + Dropbox)로 블로그 만들기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Calepin을 이용해서 &lt;a href=&quot;http://book.benelog.net이라는&quot; class=&quot;bare&quot;&gt;http://book.benelog.net이라는&lt;/a&gt; 블로그를 만들어봤습니다.그동안 여러 곳에서 정리하던 읽은 책에 대한 메모를 Markdown으로 정리한 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 .md 파일의 처음 시작에 날짜와 제목, URL, 태그 등의 정보를 달아주고, 파일을 [Dropbox 공유폴더]/Apps/Calepin이라는 폴더로 복사합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Date: 2012-07-19
Title: 어제를 버려라
Slug: 어제를_버려라
Tags: IT업계&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Calepin 사이트에 가서 &apos;Publish&amp;#8217;버튼을 누르면 약속된 폴더에서 파일정보를 읽어와서 블로그를 생성해 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/my-site/calepin.png&quot; alt=&quot;calepin&quot; title=&quot;calepin&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 정보를 바탕으로 블로그를 만들어줍니다. 아래와 같이 굉장히 단순한 디자인인데, 저는 이 정도로 충분하다고 느꼈습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/my-site/book-benelog-net.png&quot; alt=&quot;book benelog net&quot; title=&quot;book-benelog-net&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://scriptogr.am/&quot;&gt;scriptogr.am&lt;/a&gt;라는 서비스도 거의 비슷한 기능을 제공하는데, 블로그 테마 등 약간의 커스터마이징이 가능합니다.
&lt;a href=&quot;http://wikipackit.com&quot;&gt;wikipackit&lt;/a&gt;도 사용법은 유사하고, 위키 페이지간의 링크 기능을 제공하지만, 현재까지는 페이지를 비공개로만 관리할 수 있습니다.
앞으로 유사한 서비스가 더 많이 나올수도 있고, 이전 비용도 크지 않으니 언제든지 좋은 서비스가 나오면 갈아탈 생각입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스프링노트_마이그레이션&quot;&gt;스프링노트 마이그레이션&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 스프링노트로 관리하던 페이지들 중 일부는 &lt;a href=&quot;http://wikipackit.com&quot; class=&quot;bare&quot;&gt;http://wikipackit.com&lt;/a&gt; 으로 옮겼습니다. 스프링노트의 API를 이용해서 &lt;a href=&quot;https://gist.github.com/3194442#file_springnote2markdown.py&quot;&gt;Markdown형식으로 스프링노트 페이지를 다운로드하는 스크립트&lt;/a&gt;를 작성했습니다.Python과 &lt;a href=&quot;http://johnmacfarlane.net/pandoc/&quot;&gt;Pandoc&lt;/a&gt;을 활용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;eclipse_markdown_text_editor&quot;&gt;Eclipse Markdown Text Editor&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;편집기로는 주로 Eclipse에서 &lt;a href=&quot;http://marketplace.eclipse.org/content/markdown-text-editor&quot;&gt;Markdown Text Editor&lt;/a&gt;을 사용하고 있습니다.Eclipse가 무거운 편이지만, Platform Runtime Binary를 찾아서 최소한 도로 설치하면 50MB가 넘지 않게 비교적 가볍게 쓸 수도 있습니다. 예를 들어 Eclipse 3.8의 최소설치를 위한 파일은 아래 링크에서 찾으실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://download.eclipse.org/eclipse/downloads/drops/R-3.8-201206081200/&quot; class=&quot;bare&quot;&gt;http://download.eclipse.org/eclipse/downloads/drops/R-3.8-201206081200/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;편집을 하면서 HTML렌더링을 할 수도 있고, Eclipse의 단축키를 그대로 활용할 수도 있습니다.
예를 들어 파일을 찾을 때 Ctrl + Shift + R키를 눌러서 파일명의 일부를 입력해서 바로 찾아가는 것들이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/eclipse/eclipse-markdown-plugin.png&quot; alt=&quot;eclipse markdown plugin&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 구축한 글쓰기와 자료정리 환경을 굉장히 만족하면서 쓰고 있습니다.
Markdown을 지원하는 서비스나 도구들은 앞으로 계속 나올 듯하고, 스스로 만들기도 쉽기 때문에 더 편하게 쓸 수있는 가능성도 크다고 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java에서 여러줄에 걸친 문자열 선언을 편하게 하는 @Multiline</title>
      <link>https://blog.benelog.net//2999108.html</link>
      <pubDate>Tue, 29 Jan 2013 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2999108.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Andrian Walker라는 영국 개발자가 만든 라이브러리를 제가 수정해서 Github에 올렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 프로젝트에 대한 자세한 설명은 아래에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/multiline/wiki/Java%EC%97%90%EC%84%9C-%EC%97%AC%EB%9F%AC%EC%A4%84%EC%97%90-%EA%B1%B8%EC%B9%9C-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%84%A0%EC%96%B8%EC%9D%84-%ED%8E%B8%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94-%40Multiline&quot;&gt;Java에서 여러줄에 걸친 문자열 선언을 편하게 하는 @Multiline&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(github wiki에 적고 블로그에 붙여넣기를 하니 코드 블럭이 깨지는 등 편집이 쉽지가 않네요. 그래서 링크로 대신 합니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 글의 핵심 내용은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java에는 여러 줄에 걸친 문자열을 선언하는 문법이 없어서 긴 문자열을 편집하는 작업이 불편합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이를 보완하는 방법을 찾던 중 Adrian Walker라는 개발자가 만든 Multiline-string 이라는 라이브러리를 발견했고, Eclipse에서도 쓸 수 있도록 코드를 수정해서 원저자의 허락을 받고 Github에 올렸습니다. (&lt;a href=&quot;https://github.com/benelog/multiline&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/multiline&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이 과정 중에 Annotation Processing과 ECJ(Eclipse compiler for Java)에 대해서 알게 된 것들을 정리했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이 라이브러리와 비슷한 기능을 Lombok에 추가하거나, 안드로이드에서 annotation Processing을 활용할만한 방안을 더 연구해볼만 합니다&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>코딩 인터뷰 완전 분석 210쪽 17.3 변형 문제 풀이</title>
      <link>https://blog.benelog.net//2962391.html</link>
      <pubDate>Sat, 8 Sep 2012 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2962391.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;계속 이어지는 인사이트 문제입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.insightbook.co.kr/post/3876&quot; class=&quot;bare&quot;&gt;http://www.insightbook.co.kr/post/3876&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.insightbook.co.kr/wp-content/uploads/2012/08/%EC%BD%94%EB%94%A9%EC%9D%B8%ED%84%B0%EB%B7%B0-%ED%91%9C%EC%A7%801.jpg&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://www.insightbook.co.kr/wp-content/uploads/2012/08/%EC%BD%94%EB%94%A9%EC%9D%B8%ED%84%B0%EB%B7%B0-%ED%91%9C%EC%A7%801-204x300.jpg&quot; alt=&quot;image&quot; width=&quot;132&quot; height=&quot;194&quot; title=&quot;코딩인터뷰-표지&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코딩 인터뷰 완전 분석』210쪽 17.3 변형 문제&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자연수 n을 입력받고, n!의 계산 결과 중 마지막에 붙은 연속된 0의 개수와 연속된 0 바로 앞에 나오는 숫자를 구하라.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;실행_예&quot;&gt;실행 예&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;input n: 15
output: 3 8&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;설명&quot;&gt;설명&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;15!은 1307674368000이므로, 마지막에 연속된 0은 3개이고, 바로 앞의 숫자는 8이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;조건&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;n의 범위는 1 이상, 10000 이하입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;테스트 입력은 다음과 같습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;20! = 2432902008176640000
30! = 265252859812191058636308480000000
40! = 815915283247897734345611269596115894272000000000
50! = 30414093201713378043612608166064768844377641568960512000000000000
100! = 93326215443944152681699238856266700490715968264381621468592963
8952175999932299156089414639761565182862536979208272237582511852
10916864000000000000000000000000&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;프로그래밍 언어에서 제공하는 자릿수 제한 없는 곱셈을 이용하거나, 이런 형태의 곱셈 함수를 직접 구현해도 답을 얻을 수 있지만, 문제의 의도와는 다릅니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;정답 검토의 편의를 위해 블로그 포스팅에 2012!와 10000!의 결과를 남겨주세요.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(심화 문제) 연속된 0 앞에 나오는 여러 숫자를 구하는 것도 가능하니, 심심하신 분은 도전해보세요. ^^&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;먼저 입력값이 2012와 10000일 때의 결과는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;input : 2012

output: 501 8


input : 10000

output: 2499 8&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 푼 방식은 10은 5와 2의 배수라는 점을 이용했습니다.
곱해지는 숫자를 2와 5로만 소인수 분해하고  그 갯수를 각각 누적해서 구합니다.
즉 최종 팩토리안 값의 약수 중에서 2와 5가 몇 번 들어가 있는지를 계산하는 것이죠.
그리고 2,5가 아닌 나머지 약수는 구하고자하는 자릿수만큼만 잘라서 보관합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들면 5!에서 0의 갯수가 0이 아닌 마지막 숫자를 구한다면&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;5! = 1 * 2 * 3 * 4 *5 = 2^3 * 5 * 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이이므로 2는 3번, 5는 1번 들어갑니다. 0의 갯수는 2,5가 짝이 맞는 갯수이므로 둘 중에 작은 숫자인 5의 약수의 갯수, 즉 1개입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 마지막 0이 아닌 숫자는 나머지 약수의 마지막 숫자인 3과 5와 짝이 맺어지지 못한 2의 배수를 곱해서 구합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서는 약수인 2중 1개는 5와 짝이 맺어졌고, 나머지 2개가 남았으므로 2^2*3 = 12, 마지막 숫자만 남기면 2입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트 코드에서는 이미 알려진 숫자를 검증했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class FactorianAnalyzerTest {
    FactorianAnalyzer analyzer = new FactorianAnalyzer();

    @Test

    public void test5(){
        assertFactorianResult(5, 1, 2);
    }

    @Test
    public void test15(){
        assertFactorianResult(15, 3, 8);
    }

    @Test
    public void test20(){
        assertFactorianResult(20, 4, 4);
    }

    @Test
    public void test30(){
        assertFactorianResult(30, 7, 8);
    }

    @Test
    public void test40(){
        assertFactorianResult(40, 9, 2);
    }

    @Test
    public void test50(){
        assertFactorianResult(50, 12, 2);
    }

    @Test
    public void test100(){
        assertFactorianResult(100, 24, 4);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 꼭 마지막 1개의 숫자가 아닌 여러 숫자로 구할 수 있도록 확장했기 때문에 그것도 검증해보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class FactorianAnalyzerNonZeroTwoDigitsTest {

    FactorianAnalyzer analyzer = new FactorianAnalyzer();

    @Test
    public void test5(){
        assertFactorianResult(5, 2, 1, 12);
    }

    @Test
    public void test6(){
        assertFactorianResult(6, 2, 1, 72);
    }

    @Test
    public void test7(){
        assertFactorianResult(7, 3, 1, 504);
    }

    @Test
    public void test8(){
        assertFactorianResult(8, 2, 1, 32);
    }

    private void assertFactorianResult(int n,  int numOfLastNonZeroDigits, int expectedZeroCount, int expectedNonzeroDigit) {
        FactorianResult result = analyzer.countZero(n, numOfLastNonZeroDigits);
        assertThat(result.getZeroCount(), is(expectedZeroCount));
        assertThat(result.getNonZeroDigits(), is(expectedNonzeroDigit));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전체 코드는 아래 주소에 올렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/test/java/net/benelog/quiz/insight/FactorianAnalyzerTest.java&quot;&gt;FactorianAnalyzerTest.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/main/java/net/benelog/quiz/insight/FactorianResult.java&quot;&gt;FactorianResult.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/main/java/net/benelog/quiz/insight/FactorianAnalyzer.java&quot;&gt;FactorianAnalyzer.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/test/java/net/benelog/quiz/insight/FactorianAnalyzerNonZeroTwoDigitsTest.java&quot;&gt;FactorianAnalyzerNonZeroTwoDigitsTest.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>문제로 풀어보는 알고리즘 0.3 생각해보기 풀이</title>
      <link>https://blog.benelog.net//2960128.html</link>
      <pubDate>Fri, 31 Aug 2012 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2960128.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;출판사 인사이트 블로그에 아래와 같은 퀴즈가 올라왔네요
( 원문: &lt;a href=&quot;http://www.insightbook.co.kr/post/3814&quot; class=&quot;bare&quot;&gt;http://www.insightbook.co.kr/post/3814&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배열 arr[]과 위치 s, t가 있을 때,arr[s], arr[s+1], … , arr[t-1]을 오른쪽으로 한 칸씩 이동하고,arr[t]는 arr[s]로 복사하는 것을 ’1만큼 오른쪽으로 회전시켰다’고 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들어 길이가 8인 배열에서 s=2, t=6이면 다음 그림처럼 바뀐다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.insightbook.co.kr/wp-content/uploads/2012/08/%EA%B7%B8%EB%A6%BC-0-1.jpg&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://www.insightbook.co.kr/wp-content/uploads/2012/08/%EA%B7%B8%EB%A6%BC-0-1.jpg&quot; alt=&quot;image&quot; width=&quot;404&quot; height=&quot;194&quot; title=&quot;그림 0-1&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;길이가 n인 배열의 위치는 0, 1, 2, … , n-1이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문제 :k를 인자로 받아서 k만큼 오른쪽으로 회전시키는 함수를 작성하라.단, 1만큼 오른쪽으로 이동시키는 과정을 k번 반복해서는 안 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;조건 1 : 작성하는 언어에는 제한이 없습니다.
조건 2 : 답안으로 작성하신 글 제목에는 ‘문제로 풀어보는 알고리즘 0.3 생각해보기 풀이’라는 문장이 들어가야 합니다. (저희 블로그에도 트랙백을 걸어주세요.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(주의: 이 코딩 인터뷰는 인사이트 입사와는 무관합니다. ㅡㅁㅡ /)&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;밀린 일이 많아서 정신적인 여유가 없지만,  경품에 눈이 어두워서 급하게 풀어봤습니다&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우선 테스트부터 먼저 작성하고&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;

public class ArrayHandlerTest {
    ArrayHandler handler = new ArrayHandler();

    @Test
    public void testShift1() {
        int[] array = {1,2,3,4,5};
        handler.shift(array,0,3,1);
        assertThat(array, is(new int[]{4,1,2,3,5}));
    }

    @Test
    public void testShift2() {
        int[] array = {1,2,3,4,5};
        handler.shift(array,1,3,1);
        assertThat(array, is(new int[]{1,4,2,3,5}));
    }

    @Test
    public void testShift3() {
        int[] array = {1,2,3,4,5};
        handler.shift(array,1,2,1);
        assertThat(array, is(new int[]{1,3,2,4,5}));
    }
....&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 이를 통과시키는 실행코드입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.util.Arrays;public class ArrayHandler {
    public void shift(int[] array, int s, int t, int k) {
        if (s &amp;gt; t) return;
        if (t &amp;gt;= array.length) return;

        int[] rangeToShift = Arrays.copyOfRange(array, s, t + 1);
        int rangeLength = t-s + 1; for(int i=0; i&amp;lt; rangeLength; i++) {
            int offset = (i+k) % rangeLength;
            array[s + offset] = rangeToShift[i];
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대용량 배열일 때 메모리를 더 적게 쓰는 방법들도 고민해볼만도 하지만, 우선은 쉽게 문제를 푸는데 집중했습니다.
t,s, k 같은 한글자 변수명은 별로 좋아하지 않지만, 문제에 있는 변수명이라서 그대로 살렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전체 소스는 Github에 올렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;테스트 코드 : &lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/test/java/net/benelog/quiz/insight/ArrayHandlerTest.java&quot;&gt;ArrayHandlerTest.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;문제 풀이 코드 : &lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/main/java/net/benelog/quiz/insight/ArrayHandler.java&quot;&gt;ArrayHandler.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 java.util.Collections.roate()메서드를 쓰면 위 문제는 아래 두 줄로 풀 수 있기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;[source,java&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;List&amp;lt;Integer&amp;gt; range = list.subList(s, t+1);
Collections.rotate(range, k);&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 이렇게 하는게 이 문제의 의도는 아닐듯해서 참고구현 정도로 남겨 두었습니다. 처음 풀이에서도 Arrays.copyOfRange 메서드를 쓰기는 했지만 이 부분은 특별한 알고리즘이 없는 단순한 배열복사라서 활용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Collections.rotate를 이용한 풀이 코드 : &lt;a href=&quot;https://github.com/benelog/quiz/blob/master/src/main/java/net/benelog/quiz/insight/ListHandler.java&quot;&gt;ListHandler.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>버전 관리 시스템 유랑기, 그리고 Git 적응기</title>
      <link>https://blog.benelog.net//2937766.html</link>
      <pubDate>Tue, 12 Jun 2012 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2937766.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;httpsgist_github_combenelog2922437ebb284eca084_eab480eba6ac_ec8b9cec8aa4ed859c_ec9ca0eb9e91eab8b0_eab7b8eba6aceab3a0_git_eca081ec9d91eab8b0버전_관리_시스템_유랑기_그리고_git_적응기&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9C%A0%EB%9E%91%EA%B8%B0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-git-%EC%A0%81%EC%9D%91%EA%B8%B0&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9C%A0%EB%9E%91%EA%B8%B0-%EA%B7%B8%EB%A6%AC%EA%B3%A0-git-%EC%A0%81%EC%9D%91%EA%B8%B0&lt;/a&gt;버전 관리 시스템 유랑기, 그리고 Git 적응기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://onoffmix.com/event/3101&quot;&gt;2011년 6월 9일&lt;/a&gt;과 &lt;a href=&quot;http://onoffmix.com/event/4190&quot;&gt;11월 10일&lt;/a&gt;에 열린 세미나에서 두 번 발표했던 내용을 글로 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지 다양한 버전관리 시스템을 쓴 경험과 Git의 장점이라고 느낀 점에 대해서 발표했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;httpsgist_github_combenelog2922437ebb284eca084eab480eba6aceba5bc_eab1b0ec9d98_ec9588_ed9598eb8d98_ec8b9ceca088버전관리를_거의_안_하던_시절&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC%EB%A5%BC-%EA%B1%B0%EC%9D%98-%EC%95%88-%ED%95%98%EB%8D%98-%EC%8B%9C%EC%A0%88&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC%EB%A5%BC-%EA%B1%B0%EC%9D%98-%EC%95%88-%ED%95%98%EB%8D%98-%EC%8B%9C%EC%A0%88&lt;/a&gt;버전관리를 거의 안 하던 시절&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437ebb284eca084eab480eba6aceb8a94_eba8bc_eb8298eb9dbcec9d98_ec9db4ec95bceab8b0버전관리는_먼_나라의_이야기&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC%EB%8A%94-%EB%A8%BC-%EB%82%98%EB%9D%BC%EC%9D%98-%EC%9D%B4%EC%95%BC%EA%B8%B0&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC%EB%8A%94-%EB%A8%BC-%EB%82%98%EB%9D%BC%EC%9D%98-%EC%9D%B4%EC%95%BC%EA%B8%B0&lt;/a&gt;버전관리는 먼 나라의 이야기?&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git에 대해 이야기를 하기 전에 지금까지 어떻게 버전관리를 해왔는지 되돌아보겠습니다. 아마 비슷한 경험을 하시고 공감하실 분들이 많으실 듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버전관리 시스템을 실무에서 쓰기 전에는 책에서 간단한 소개를 접한 정도였습니다. 김익환님이 지은 &lt;a href=&quot;http://www.yes24.com/24/goods/416619&quot;&gt;&apos;대한민국에는 소프트웨어가 없다&apos;&lt;/a&gt; 라는 책에는 아래와 같은 내용이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;내가 아무리 소스코드관리 프로그램과 버그 관리 프로그램의 찬양자가 되어 조언을 해도 평생 사용하지 않을 회사들이 대부분일 것이다. 이건 진짜 좋은 건데. 안타깝다(153쪽)&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책이 나온 때가 2003년 말이였는데, 그 당시에 국내에서 버전관리를 제대로 사용하는 개발 조직은 많지 않았나봅니다. 제 주변에서도 그랬습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그때는 버전관리가 실무에서 필요하다기 보다 &apos;CMMI&apos; 같은 프로세스처럼 이론으로만 강조되는 분야라고 느꼈습니다. 아니면 엄격한 프로세스를 준수하고 그만큼 개발자에게 시간을 충분히 주는 소프트웨어 선진국에서나 가능한 기법이라고 생각했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;김익환님이 미국에서 근무했던 &apos;GTE&amp;#8217;라는 조직에서는 개발자들마다 코딩하는 줄 수를 측정했더니 하루 평균 8줄이 나왔다는 내용이 있습니다.(171쪽). 그정도로 엄격한 리뷰와 관리를 거친다는데, 하루에 화면 4~5개씩 찍어내야하는 제 주변의 현실과는 동떨어진 이야기였습니다. 그래서 버전관리도 우리와는 다른 방식으로 개발을 하는 먼 나라의 일로, 우리와는 관계가 없는 절차로만 여겼습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437eab09cebb09cec849cebb284ec9790_ed8c8cec9dbcec9db4_ec9e88eb8a94eb8db0_ec999c_eb9890_ed8c8cec9dbcec9d84_eb94b0eba19c_eab480eba6aced95a0eab98c개발서버에_파일이_있는데_왜_또_파일을_따로_관리할까&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EA%B0%9C%EB%B0%9C%EC%84%9C%EB%B2%84%EC%97%90-%ED%8C%8C%EC%9D%BC%EC%9D%B4-%EC%9E%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EB%98%90-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%94%B0%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%A0%EA%B9%8C&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EA%B0%9C%EB%B0%9C%EC%84%9C%EB%B2%84%EC%97%90-%ED%8C%8C%EC%9D%BC%EC%9D%B4-%EC%9E%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EB%98%90-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%94%B0%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%A0%EA%B9%8C&lt;/a&gt;개발서버에 파일이 있는데 왜 또 파일을 따로 관리할까?&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;신입 때 처음 투입되었던 프로젝트에서는 버전관리를 하기는 했지만, 공유폴더보다 더 불편하다는 느낌 뿐이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 프로젝트에서는 Local PC에 개발환경을 설치하지 않고, 개발서버에 FTP로 jsp 파일를 올려서 개발을 진행했었습니다. JSP로만 주로 개발을 하던 초기에는 흔한 방식이였습니다. 그래서 개발서버에 이미 최신 파일이 올라가 있는데, 따로 왜 버전관리 시스템이라는 것을 써야하는지 이해가 되지 않았습니다. 가끔 실수로 파일을 덮어쓸 때에는 백업 폴더에 가서 복사해서 복구했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 조직에도 Merant Version Manager라는 버전관리 시스템이 설치되어 있기는 했습니다.그 도구를 개발자들이 필요해서 쓴다기보다는 본사에서 시키니까 쓰기는하는데, 뭔가 부가적인 작업을 한다고 느꼈습니다. 개발서버에 파일 올려서 테스트를 다 해본다음에, 버전관리 시스템에 로그인하고, PC에 있는 파일 찾아서 올리고, Local에서도 여러 군대의 파일을 같이 신경써줘야 했습니다. 귀찮고 그냥 빼먹고 싶은 작업이였습니다. 그때는 지금의 SVN처럼 Eclipse plugin이 있는 것도 아니어서 따로 클라이언트 프로그램을 띄워야했기에 더욱 불편했었습니다. 그래서인지 두번 째 투입되었던 조직에서는 개발서버에만 파일을 올려놓고 아예 버전관리를 안 했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437eab3b5ec9ca0ed8fb4eb8d94ec9790_ec98aceba0a4eca3bcec8b9ceba9b4_eca080eb8581_6ec8b9cec9790_ebb0b0ed8faced95b4_eb939ceba6bdeb8b88eb8ba4공유폴더에_올려주시면_저녁_6시에_배포해_드립니다&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EA%B3%B5%EC%9C%A0%ED%8F%B4%EB%8D%94%EC%97%90-%EC%98%AC%EB%A0%A4%EC%A3%BC%EC%8B%9C%EB%A9%B4-%EC%A0%80%EB%85%81-6%EC%8B%9C%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%B4-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EA%B3%B5%EC%9C%A0%ED%8F%B4%EB%8D%94%EC%97%90-%EC%98%AC%EB%A0%A4%EC%A3%BC%EC%8B%9C%EB%A9%B4-%EC%A0%80%EB%85%81-6%EC%8B%9C%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%B4-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&lt;/a&gt;공유폴더에 올려주시면 저녁 6시에 배포해 드립니다~&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세번째 프로젝트에서는 요즘처럼 Local PC에 WAS를 띄우고 Eclipse를 써서 개발을 하기는 했지만, 윈도우즈의 공유 폴더로 소스 관리를 했었습니다. 그때 완성된 프로그램을 서버에 배포하는 절차는 다음과 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;개발이 끝난 파일은 공유폴더의 약속한 위치에 올린다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;담당자 한명이 매일 그 폴더의 파일을 자기 PC로 복사하고, Eclipse에서 컴파일한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;컴파일 에러가 나는 것이 있으면 그 파일을 올렸을 것으로 추측되는 사람에게 이야기해서 고쳐달라고 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;컴파일 에러 안 나면 Eclipse에서 컴파일된 class파일과 JSP등을 FTP로 개발서버에 올리고 서버를 재시작한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;서버 재시작이 끝나면 개발자들이 각자 자기가 만든 모듈을 확인해본다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;개발서버에서 잘 안 되는 기능 중에 급한 것은 다시 고쳐서 공유폴더에 넣고, 미안하지만 한번더 올려달라고 담당자에게 부탁한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;안 급한 것은 그냥 내일 반영하기로 하고, 일단 Local PC에서만 고쳐둔다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파일서버의 backup 폴더 아래에 날짜별로 오늘 배포한 것은 복사해 둔다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;매일매일 위의 절차를 반복하는데 30분을 넘게 썼었습니다. 프로젝트 구성원들 모두 개선을 해보려는 생각은 있었지만 당장의 업무가 급하다보니 투자할 시간이 없었습니다. 돌이켜 보면 프로젝트 개발기간이 6개월이 넘었는데, 6개월동안 매일 30분이면 엄청난 시간이 들어가는 작업이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 프로젝트에서도 버전관리시스템인 Merant Version Manager를 본사의 담당자가 와서 설치해주고,고객와 개발자들을 다 모아서 따로 교육까지 해주었습니다.그러나 역시나 버전관리 시스템에 파일을 올리는 건 파일서버에 복사를 하는 것보다 번거로운 일이라고 생각했었습니다. 그래서 버전관리 서버에는 1차 프로젝트 종료 후에 몰아서 한번 파일을 올리기로 합의했고, 그 방식이 효율적이라고 모든 프로젝트 구성원들이 공감을 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그나마 개발 영역별로 업무분담이 명확해서 파일 충돌은 잘 일어나지 않았습니다. 충돌보다는 작업한 내용을 실수로 날릴까봐 걱정하는 사람은 많았는데, 어떤 개발자는 매일 퇴근 전에 개인 PC에 날짜별로 복사해 놓기도 했었습니다. 다들 그 개발자를 보고 부지런하고 좋은 습관을 가지고 있다고 칭찬을 했었죠. 이 기간 동안에도 공유폴더에 만족을 하고 버전관리는 업무를 더 느리게 하는 작업이라고 인식했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;httpsgist_github_combenelog2922437merant_version_managerec9d84_ec93b0eb8d98_ec8b9ceca088merant_version_manager을_쓰던_시절&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#merant-version-manager%EC%9D%84-%EC%93%B0%EB%8D%98-%EC%8B%9C%EC%A0%88&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#merant-version-manager%EC%9D%84-%EC%93%B0%EB%8D%98-%EC%8B%9C%EC%A0%88&lt;/a&gt;Merant Version Manager을 쓰던 시절&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437ebb98ceb939c_ec9e90eb8f99ed9994_eab7b8eba6aceab3a0_merant_version_managereba5bc_eca09ceb8c80eba19c_ec82acec9aa9ed9598eab8b0_ec8b9cec9e91ed9598eb8ba4빌드_자동화_그리고_merant_version_manager를_제대로_사용하기_시작하다&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%B9%8C%EB%93%9C-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-merant-version-manager%EB%A5%BC-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%B9%8C%EB%93%9C-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B7%B8%EB%A6%AC%EA%B3%A0-merant-version-manager%EB%A5%BC-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4&lt;/a&gt;빌드 자동화, 그리고 Merant Version Manager를 제대로 사용하기 시작하다.&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇게 공유폴더를 쓰던 개발팀에서 고도화 성격의 2차프로젝트가 시작되었습니다.운영 중인 시스템에서 몇가지 업무가 추가되는 사업이였는데, 운영 중인 시스템에 당장 배포할 소스와 2차 프로젝트 오픈 때 배포할 소스를 따로 관리해야 했습니다. 유지보수성 요청 때문에 수정된 소스는 바로 운영 시스템에 배포를 하지만, 2차 사업에 추가요구사항으로 개발된 소스는 오픈 시점에 맞춰서 운영서버에 배포해야 했습니다. 그래서 유지보수와 신규개발 버전을 따로 브랜치를 분리할 필요성이 생겼습니다. 그리고 전에는 담당자별로 업무영역이 명확하게 나뉘어져 있었는데, 새로운 프로젝트에서는 그런 경계가 명확하지 않은 부분이 많이 생겼습니다.그런 필요성 때문에 버전관리를 본격적으로 시작하게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 시점에서 빌드자동화 스크립트도 구성했습니다. 수동으로 해서 매일 30분이 걸리던 빌드절차를 개선하는 시도였었습니다. 파일서버에 따로 복사한 파일이 아닌, 버전관리에 올라온 최신 소스 파일을 읽어서 컴파일하고 서버에 FTP로 올리고, WAS를 재시작하는 스크립트를 ANT로 구성했습니다. 제가 한 3일정도 걸려서 그 작업을 했었는데, 처음 맨바닥에서 하는 일이라서 지금의 기준으로 보면 정말 많은 시간이 걸렸습니다. 지금은 Maven + Hudson으로 하면 몇십분 안에 가능한 작업이지만, 그래도 3일 투자해서 매일매일 30분의 작업을 없애서 정말 만족스러웠습니다.그때부터 다른 개발자들도 이제 공유폴더에 파일을 복사해 넣는 대신 매일매일 버전관리시스템인 Merant Version Manager에 파일을 올리기 시작했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437_ec9db4_eb8c80ebb680ebb684ec9db8_commit_eba19ceab7b8_이_대부분인_commit_로그&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#-%EC%9D%B4-%EB%8C%80%EB%B6%80%EB%B6%84%EC%9D%B8-commit-%EB%A1%9C%EA%B7%B8&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#-%EC%9D%B4-%EB%8C%80%EB%B6%80%EB%B6%84%EC%9D%B8-commit-%EB%A1%9C%EA%B7%B8&lt;/a&gt;&apos;.&apos; 이 대부분인 commit 로그&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 단계부터는 단순하게 파일시스템으로 백업을 하는 시대는 지났지만, 버전관리를 깊이 있게 사용하지는 못했습니다.그때 대부분의 커밋로그를 보면 점하나 (.)만 찍은 것이 많았습니다. 아무런 내용을 안 쓰면 입력하라고 메시지가 나오니까 그 걸 피하려고 넣은 문자가 &apos;.&apos;점이였습니다. 시스템을 운영하고, 담당자가 바뀌고, 업무 규칙이 바뀌어갈수록 버전관리 시스템에 있는 커밋로그가 정말 소중한데, 그때는 그런 걸 알지 못했습니다. SI프로젝트이다 보니 빨리 개발 다 끝내고 도망갈 생각이 먼저이지, 운영할 사람에게 도움이 되는 정보를 남겨야한다는 생각은 잘 나지 않았습니다.한참 지난 다음의 이야기지만, 불행스럽게도 제가 그 프로젝트에서 마지막까지 남아서 유지보수를 했습니다. 물론 그 덕분에 중간에 나갔으면 몰랐을 것들을 많이 느끼긴했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437lock_eab1b8eab3a0_ed87b4eab7bclock_걸고_퇴근&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#lock-%EA%B1%B8%EA%B3%A0-%ED%87%B4%EA%B7%BC&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#lock-%EA%B1%B8%EA%B3%A0-%ED%87%B4%EA%B7%BC&lt;/a&gt;Lock 걸고 퇴근&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 처음 버전관리 시스템으로 썼던 Merant Version Manager는 CVS처럼 배타적인 방식으로 파일의 소유권을 관리했었습니다. 즉, 고칠파일이 있으면 먼저 그 파일들을 &apos;Checkout&amp;#8217;해야 하는데, 그 상태에서는 다른 사람은 그 파일을 건드리지 못하게 됩니다. 수정이 끝난 후에 &apos;Check in&amp;#8217;을 하면 고친 파일이 올라가고, 다른 사람이 &apos;Check out&amp;#8217;을 받을 수 있는 상태로 변합니다. 그 제품의 최신 버전은 어떤지는 몰라도 당시에는 그렇게 썼었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;몇번은 다른 개발자가 파일을 Checkout 해놓은 상태로 퇴근을 했는데, 급하게 파일을 고쳐야 할 때가 있었습니다. 전화해서 비밀번호를 물어보고 해결을 했는데, 나중에는 업무가 밀접하게 관련있는 사람들끼리는 비밀번호를 거의 공유하다시피 했었습니다. 그리고 Lock 걸린 파일을 수정해야 할 때도 급한 일이 아니라면 먼저 &apos;Check out&amp;#8217;한 사람이 다 고칠 때까지 기다렸었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그때는 그게 당연한 방식이였고, 그렇게 해야 질서가 있고 안전하다고 생각했습니다. 얼핏 SVN이 lock없이 파일을 고쳐도 된다고 듣기는 했지만, 그렇게 하면 소스가 엉켰을 때 해결이 되지 않을 위험한 방식이라고 추측했습니다. 당시에는 그 버전관리 방식에 역시나 만족을 했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;httpsgist_github_combenelog2922437svnec9d84_ec93b0eab8b0_ec8b9cec9e91ed9598eb8ba4svn을_쓰기_시작하다&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#svn%EC%9D%84-%EC%93%B0%EA%B8%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#svn%EC%9D%84-%EC%93%B0%EA%B8%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8B%A4&lt;/a&gt;SVN을 쓰기 시작하다&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437svn_ec9e85ebacb8svn_입문&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#svn-%EC%9E%85%EB%AC%B8&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#svn-%EC%9E%85%EB%AC%B8&lt;/a&gt;SVN 입문&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러다가 직장이 바뀌어서 SVN을 쓰는 개발조직에서 일하게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SVN을 특별히 공부한 적이 없었는데도 &lt;a href=&quot;http://subclipse.tigris.org/&quot;&gt;Subclipse&lt;/a&gt;나 &lt;a href=&quot;http://www.eclipse.org/subversive/&quot;&gt;Subversive&lt;/a&gt;같은 Eclipse plugin 덕분에 금방 적응을 했습니다. 그런 Plugin 덕분에 버전관리가 부가적인 작업이 아니고, 개발과정에 자연스럽게 녹아든다는 느낌이 들었습니다.배타적이지 않은 lock관리 방식과 &apos;transactional&amp;#8217;한 특성을 가진 commit 방식도 직접 써보니 더 편리한 개념이라는 것을 깨달았습니다. 가끔 Eclipse plugin의 버그나 뭔가 상태가 꼬였을 때 헤매기도 했지만, 얼마 지나지 않아 비슷한 상황에서의 문제해결에 익숙해지고, Plugin도 점점 안정화되어서 큰 불편없이 쓰게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SVN을 쓰기 전까지는 이전의 버전관리 방식에 만족을 하고 있었는데, 막상 써보니 이전에는 그 불편함을 어떻게 견디었나하는 생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437eba8b8eca780eb8a94_ec96b8eca09ceb8298_eb9190eba0a4ec9ab4_ec9dbc머지는_언제나_두려운_일&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%A8%B8%EC%A7%80%EB%8A%94-%EC%96%B8%EC%A0%9C%EB%82%98-%EB%91%90%EB%A0%A4%EC%9A%B4-%EC%9D%BC&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%A8%B8%EC%A7%80%EB%8A%94-%EC%96%B8%EC%A0%9C%EB%82%98-%EB%91%90%EB%A0%A4%EC%9A%B4-%EC%9D%BC&lt;/a&gt;머지는 언제나 두려운 일&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 SVN에서는 브랜치 관리나 머지가 여전히 번거롭거나 두려운 일이였습니다.특히나 브랜치를 생성하고 오랫동안 유지하는 일은 정말 손이 많이 가는 일이였습니다.장기 프로젝트를 했을 때는 브랜치가 분기된 이후로 트렁크(trunk)에 반영된 사안들을 다시 브랜치로 옮기는 작업을 주기적으로 했는데, 자주 안하면 나중에 최종 머지가 두려워지고, 그렇다고 자주하기에는 시간이 많이 걸리는 작업이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;브랜치 관리와 머지를 잘하기 위해서 여러 개발팀에서 다양한 방법을 쓰고 있는 것을 보았습니다. 어떤 프로젝트에서는 일주일에 한번씩 트렁크 쪽의 이력을 가장 많이 알고 있는 사람과 브랜치를 가장 많이 수정한 사람이 같이 앉아서 작업을 했는데, 오래 걸리면 한번에 30~40분이 넘게 걸리기도 했습니다.팀에서 그런 머지작업을 많이 한 사람이 &apos;머지 전문가&amp;#8217;로 불리기도 했습니다. 당시 썼던 SVN버전에서는 머지한 후에 그동안의 이력이 남지 않기 때문에, 머지를 수정한 커밋 로그에는 &quot;from revision 10244 to 10532&quot; 와 같은 형식으로, 어디서부터 어디까지를 합쳤는지 로그를 적었습니다. 머지에 쓰는 도구도 사람마다 팀마다 다양했습니다. &lt;a href=&quot;http://subclipse.tigris.org/&quot;&gt;Subclipse&lt;/a&gt;나 &lt;a href=&quot;http://www.eclipse.org/subversive/&quot;&gt;Subversive&lt;/a&gt;의 메뉴를 활용하는 팀이 있었는데, 각자 개인마다 나름대로의 노하우가 있는 듯했습니다.어떤 팀에서는 그런 도구를 이용하면 오히려 실수가 많다면서 2개의 브랜치를 모두 Eclipse에서 띄운 후에 Diff로 하나하나 비교해가면서 파일도 하나하나 복사해서 가는 방식을 썼었습니다.시간이 상당히 많이걸리는 방식이였지만, 그 팀에서는 머지는 그렇게 하는게 최고라면서 상당히 만족하고 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오랜 노하우가 쌓인다고 해도 어떤 방식이든 여전히 실수할 여지는 많았습니다. 머지되어야할 이력이 빠져서 오류가 발생했던 경험도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇게 브랜치를 관리하는 부담이 크다보니, 규모가 큰 변경이 아니라면 왠만해서는 브랜치를 안 따고 Trunk만 사용하고 싶었습니다.그런데 개발서버에라도 언제 배포될지도 모르는 Trunk에 마음대로 커밋을 하는 것은 부담이 되는 일이였습니다. 부분적으로 잘 안 돌아가는 소스를 커밋할 수는 없으니,어느 정도 기능별로 완결된 것만 커밋을 하게 되었고, 그러다보면 커밋을 자주하지 않게 되기도 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;박재성님이 쓰신 &lt;a href=&quot;http://www.yes24.com/24/Goods/3301419&quot;&gt;Java 프로젝트 필수유틸리티&lt;/a&gt;라는 책에서도 아래와 같이 SVN merge의 어려움에 대해서 나와있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SVN에서 소스 코드 머지가 어려워 브랜치를 사용하지 않고 신규 메소드를 추가하는 방식으로 변경할 수 밖에 없는 상황도발생한다. 그렇지 않아도 VCS의 사용에 반대하는 개발자들이 있는 상황에서 머지의 어려움은 개발자들을 설득하기 어렵게 한다.(307쪽)&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 문단을 포함해서 이 책의 306~307쪽에는 머지를 한후 커밋 로그에 리비전 번호를 기록하는 팁이라던지, SVN에서 merge를 하다가 문제를 겪으신 분의 이야기가 나와 있습니다.( &lt;a href=&quot;http://ahnyounghoe.tistory.com/5#comment670941&quot;&gt;안영회님의 블로그 글에 달린 강수형님의 댓글&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SVN의 최신버전에서는 머지를 하면 SVN에서 알아서 따로 추가적인 정보를 남기도록 개선이 되었다고는 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;httpsgist_github_combenelog2922437giteba5bc_eba79bebb3b4eb8ba4git를_맛보다&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#git%EB%A5%BC-%EB%A7%9B%EB%B3%B4%EB%8B%A4&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#git%EB%A5%BC-%EB%A7%9B%EB%B3%B4%EB%8B%A4&lt;/a&gt;Git를 맛보다&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437dvcsec9790_eb8c80ed959c_ec868cebacb8eba78c_eb93a3eb8ba4eab080dvcs에_대한_소문만_듣다가&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#dvcs%EC%97%90-%EB%8C%80%ED%95%9C-%EC%86%8C%EB%AC%B8%EB%A7%8C-%EB%93%A3%EB%8B%A4%EA%B0%80&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#dvcs%EC%97%90-%EB%8C%80%ED%95%9C-%EC%86%8C%EB%AC%B8%EB%A7%8C-%EB%93%A3%EB%8B%A4%EA%B0%80&lt;/a&gt;DVCS에 대한 소문만 듣다가..&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그럭저럭 SVN에 만족했지만 브랜치 관리에는 다소 아쉬움이 있었던 차에, 여러 오픈소스 프로젝트들이 Git이나 Mercurial같은 분산버전관리시스템(DVCS)로 옮기어 간다는 이야기를 들었습니다.인터넷에도 분산버전관리 시스템에 대한 글들이 많이 올라왔는데, 그 중에서 &lt;a href=&quot;http://surpreem.com/archives/51&quot;&gt;조엘 스폴스키가 쓴 Mercurial 튜터리얼&lt;/a&gt;이 인상 깊었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래도 직접 써본적이 없으니, 어떤 점이 좋을지 막연하게 상상한 하는 정도였습니다.그러던 중 팀내부에서 진행하는 프로젝트에서 Git으로 버전관리를 하게 되면서, Git의 좋은 점들을 직접 체험하게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437ecb2b4eab090ed959c_ec9ea5eca090체감한_장점&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EC%B2%B4%EA%B0%90%ED%95%9C-%EC%9E%A5%EC%A0%90&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EC%B2%B4%EA%B0%90%ED%95%9C-%EC%9E%A5%EC%A0%90&lt;/a&gt;체감한 장점&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래에 정리한 Git의 장점들은 다른 누군가가 그렇다더라.. 해서 적은 것이 아니고, 실제로 프로젝트를 하면서 솔직하게 느껴졌던 것들입니다.오랫동안 쓰다보면 이것보다 더 많은 것을 느끼겠지만, 처음 써보는 프로젝트에서는 이정도가 개발하는데 도움이 된다고 생각되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect4&quot;&gt;
&lt;h5 id=&quot;httpsgist_github_combenelog2922437ctrlz_ed8c8cec9dbc_ebb3b5ec82acebb3b4eb8ba4_ec9588ec8bacec9db4_eb9098eb8a94_local_ebb284eca084_eab480eba6acctrlz_파일_복사보다_안심이_되는_local_버전_관리&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#ctrlz-%ED%8C%8C%EC%9D%BC-%EB%B3%B5%EC%82%AC%EB%B3%B4%EB%8B%A4-%EC%95%88%EC%8B%AC%EC%9D%B4-%EB%90%98%EB%8A%94-local-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#ctrlz-%ED%8C%8C%EC%9D%BC-%EB%B3%B5%EC%82%AC%EB%B3%B4%EB%8B%A4-%EC%95%88%EC%8B%AC%EC%9D%B4-%EB%90%98%EB%8A%94-local-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC&lt;/a&gt;Ctrl+Z, 파일 복사보다 안심이 되는 Local 버전 관리&lt;/h5&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개발자의 Local PC에서도 버전관리를 할 수 있다는 점은 DVCS의 대표적인 장점으로 꼽히고 있습니다.그런데 과연 그것이 꼭 필요한지, 오히려 버전관리 과정을 더 복잡하게 하지 않는지 우려하시는 분이 많고, 저도 그랬었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실제로 프로젝트를 해보니 지금까지 &apos;Ctrl + Z&amp;#8217;나 주석문으로 가리기, 파일복사로 해왔던 작업 중에 어떤 부분은 Local에서의 버전관리로 해결할 부분이 아니었나하는 생각이 듭니다.코드를 작성하다보면 성공의 확신이 없는 시도를 해볼 때도 있고, 시도한 것이 여의치 않았을 때 다시 돌아올 수 있는 지점을 기록하고 싶어지기도 합니다.물론 간단한 1~2줄의 수정 같은 정도는 Ctrl+Z, 주석문 등을 이용하는 것이 더 편리할 수도 있습니다. 그러나 파일의 여러 부분이 동시에 바뀌어야 하는데 나중에 되돌릴 가능성이 있다던지, 두가지 방식의 구현을 시험삼아서 같이 진행할 때는 Local에서 중간중간 commit을 하거나 브랜치를 생성하는 편이 훨씬 편리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SVN에서는 따로 브랜치를 따고, 나중에 머지하는 일이 번거롭고 두렵기 때문에 통합할 가능성이 확실하지 않은 수정은 Local PC에서 원래의 파일을 복사본을 만들어서 수정하기도 했었습니다. 그러나 Git에서는 Local branch 생성과 이동이 빠르고 결과를 머지하는 것도 더 안정감이 있다는 느낌이 들기 때문에, Ctrl + Z,주석문 처리(//), 파일 복사로 때우던 소규모의 변경 지점 관리가 더 편해집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git을 썼던 프로젝트에서 XML파싱을 하는 모듈을 개발했었는데, JDOM과 DOM4J중에서 어떤 것이 해당 용도에 적합할지 확신이 없는 상태에서 2개의 Local branch를 만들어서 작업을 했었습니다.처음에 JDOM으로 갔다가, 마음에 안 드는 부분이 있어서 DOM4J로 구현했다가 다시 JDOM으로 돌아왔는데, 이렇게 결정을 번복하는 과정에서 Local의 버전관리가 없었다면 훨씬 번거롭고 다른 작업자들에게도 방해가 되었을 듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect4&quot;&gt;
&lt;h5 id=&quot;httpsgist_github_combenelog2922437ebb284eca084eab8b0eba19deab3bc_ed86b5ed95a9ec9d98_eab09ceb8590_ebb684eba6ac버전기록과_통합의_개념_분리&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84%EA%B8%B0%EB%A1%9D%EA%B3%BC-%ED%86%B5%ED%95%A9%EC%9D%98-%EA%B0%9C%EB%85%90-%EB%B6%84%EB%A6%AC&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%B2%84%EC%A0%84%EA%B8%B0%EB%A1%9D%EA%B3%BC-%ED%86%B5%ED%95%A9%EC%9D%98-%EA%B0%9C%EB%85%90-%EB%B6%84%EB%A6%AC&lt;/a&gt;&apos;버전기록&amp;#8217;과 &apos;통합&amp;#8217;의 개념 분리&lt;/h5&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 말했듯이, Local에서 버전관리가 가능하기 때문에 &apos;버전기록&amp;#8217;과 &apos;통합&amp;#8217;이 다른 개념이 됩니다.SVN에서 &apos;커밋&amp;#8217;은 &apos;버전기록&amp;#8217;이자 &apos;소스 통합&amp;#8217;을 의미합니다.. 즉, 작업한 내용을 기록하는 일과 다른 사람이 일한 소스와 합치는 일이 같은 일입니다.저도 지금까지 그것이 당연하다고 생각해왔기 때문에 두가지를 같이 취급하는 것이 전혀 어색하지 않았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 엄밀히 따지면 2가지는 구분이 됩니다. 버전기록은 중간중간 의미 있는 단위의 작업이 끝날 때마다 할만한 일입니다. 그에 반해서 통합은 내가 작업한 소스를 다른 작업자들이 받아가도 괜찮은 시점에 하는 것이 바람직합니다. 예를 들어 컴파일도 되지 않는 소스를 커밋을 하고 그 에러를 고치는 커밋을 하기 전에 다른 동료가 소스를 받아간다면, 동료는 원래 하는 일에 방해를 받게 됩니다.SVN에서는 &apos;커밋=통합&amp;#8217;이라서 개인이 작업한 것을 전체 소스에 통합을 해도 괜찮은 시점에만 커밋을 해야 그런 부작용이 없습니다.만약 작업하는 브랜치가 언제라도 배포될 수 있는 브랜치라면 극단적으로는 개발작업에 완료된 시점에만 커밋을 해야 안전합니다. 이렇게 &apos;커밋=통합&amp;#8217;일 때는 버전관리의 모든 욕구를 다 충족시키지 못할 수도 있습니다. 오류가 있거나, 아직 확정되지 않은 인터페이스를 포함하고 있어서 다른 사람에게 전파되지 않았으면 하는 프로그램의 상태라도 버전기록은 하고 싶은 상황도 생길 수 있습니다.많은 경우, 통합보다 버전기록이 더 자주 필요한 일입니다.그리고 통합은 버전기록보다 더 신중하고 노력이 많이 들어가는 일입니다.예를 들면 안전한 통합을 위해서는 svn update &amp;#8594; mvn test &amp;#8594; commit의 절차를 거쳐야하는데, 버전기록이 필요할 때마다 위와 같이 안전한 통합을 위한 절차를 할지말지 고민해야 한다면 버전기록은 훨씬 무거운 일이됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;물론 SVN으로도 별도의 브랜치를 딴다면 다른 작업자에게 영향없이 독립적으로 버전기록을 할 수 있지만, 굉장히 번거롭습니다.Git에서는 버전기록이 필요한 시점에서는 언제든지 commit을 하고, 통합할만한 상태가 되었을 때만 push를 하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 분은 통합 없는 버전기록을 할 수 있다면 통합을 자주 하지 않게 된다는 우려를 하시기도 합니다.그런데 SVN으로 작업할 때도 통합 가능한 상태를 염두에 두지 않고 버전기록이 필요할 때마다 자주 commit을 하다보면 CI서버의 빌드가 자주 깨지게 됩니다.빌드 실패는 빠른 피드백으로서 의미가 있지만, 너무 자주 실패하면 잡음이 되어서 다른 사람에게 방해가 되거나, 그 신호를 무시하게 됩니다.그런 부작용을 우려해서 오히려 commit을 자주 하지 않게 되는 경우를 보기도 했었습니다. 그래서 어떤 프로젝트에서는 &apos;적어도 하루에 한번씩은 commit을 하자&amp;#8217;라고 규칙을 정한 곳도 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버전기록과 통합을 강제로 묶어서 통합을 자주 하기를 바라기보다는 두 가지 개념을 구분하고 어떻게 활용할지는 상황에 따라서 선택하는 편이 좋다고 생각합니다.버전기록과 통합이 강제로 묶여있다면 개발자들이 오히려 둘 다 자주 하지 않게 될 수도 있습니다.통합을 자주하고 싶다면 필수적인 통합주기를 개발팀에서 약속을 하는 편이 더 실용적입니다.
기술 도메인의 특성이나 작업을 어떻게 분담하느냐에 따라서 통합과 버전관리의 주기는 다양할 것입니다.보편적인 버전관리 도구라면 상황에 따라서 다양한 정책을 적용할 수 있도록 개념이 섬세하고 유연한 편이 더 좋다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect4&quot;&gt;
&lt;h5 id=&quot;httpsgist_github_combenelog2922437ebb9a0eba5b4eab3a0_ed8eb8ed959c_ebb88ceb9e9cecb998_ec9db4eb8f99빠르고_편한_브랜치_이동&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%B9%A0%EB%A5%B4%EA%B3%A0-%ED%8E%B8%ED%95%9C-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%9D%B4%EB%8F%99&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%B9%A0%EB%A5%B4%EA%B3%A0-%ED%8E%B8%ED%95%9C-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%9D%B4%EB%8F%99&lt;/a&gt;빠르고 편한 브랜치 이동&lt;/h5&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어느 정도 규모가 있는 프로젝트에 참여하다보면 여러 브랜치를 왔다갔다 하면서 작업할 일이 많습니다. SVN에서도 Local에 받은 소스 폴더를 다른 브랜치로 이동을 할 수 있고, Eclipse plugin으로 편하게 지원됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Switch to branchhttps://camo.githubusercontent.com/14111eced471ead7d87404b86e3d337f404dce42/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f73766e5f7377746963685f6272616e63682e706e67[&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://camo.githubusercontent.com/14111eced471ead7d87404b86e3d337f404dce42/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f73766e5f7377746963685f6272616e63682e706e67&quot; alt=&quot;SVN switch branch&quot;&gt;&lt;/span&gt;]&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, 저는 프로젝트를 하면서 저 기능을 거의 쓴 적이 없습니다. 네트워크로 다른 브랜치의 파일을 받아오기 때문에 이동하는 속도가 굉장히 느리기 때문입니다.그리고 원래 있던 브랜치에서 커밋을 아직 안 한 파일이 남아 있어도 이동이 안 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 불편함 때문에 보통 여러 branch를 Eclipse에서 별도의 프로젝트로 받아서 한꺼번에 열어두는 일이 많습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://camo.githubusercontent.com/8bf54e103af68913b7d606dea0af60e45048f57a/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f73766e5f6272616e636865732e706e67&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://camo.githubusercontent.com/8bf54e103af68913b7d606dea0af60e45048f57a/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f73766e5f6272616e636865732e706e67&quot; alt=&quot;SVN branches&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러다보면 파일을 수정할때 어느 브랜치의 파일을 수정하는 건지 헷갈릴 때가 많습니다. 보통 저는 Ctrl + Shift + R로 파일을 찾는데, 여러 브랜치를 동시에 열고 있을 때는 같은 파일이름이 동시에 떠서 불편합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://camo.githubusercontent.com/63777e9ff0cc1771b81a86b4e3e0766a2b4e77cd/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f6f70656e5f7265736f757263652e706e67&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://camo.githubusercontent.com/63777e9ff0cc1771b81a86b4e3e0766a2b4e77cd/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f6f70656e5f7265736f757263652e706e67&quot; alt=&quot;Open Resources&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 여러 브랜치가 열려있는 상황에서 의도하지 않은 브랜치의 파일을 고치는 실수는 주변에서 굉장히 흔하게 보였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git에서는 이런 브랜치 전환이 굉장히 빠릅니다. 네트워크를 타지 않고 Local에서 branch를 전환을 하니 당연한 일입니다.Git을 처음 써본 프로젝트에서도 몇번 브랜치를 전환하면서 작업을 했었는데, 하나의 프로젝트만 열고만 있으면 되니 훨씬 편리하고 빨랐습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect4&quot;&gt;
&lt;h5 id=&quot;httpsgist_github_combenelog2922437eb8d94_eca095eab590ed959c_ecbba4ebb08b_eba19ceab7b8더_정교한_커밋_로그&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%8D%94-%EC%A0%95%EA%B5%90%ED%95%9C-%EC%BB%A4%EB%B0%8B-%EB%A1%9C%EA%B7%B8&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%8D%94-%EC%A0%95%EA%B5%90%ED%95%9C-%EC%BB%A4%EB%B0%8B-%EB%A1%9C%EA%B7%B8&lt;/a&gt;더 정교한 커밋 로그&lt;/h5&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SVN에서는 브랜치에 있던 파일을 트렁크에 머지하면 그동안 브랜치에 커밋한 이력은 옮겨지지 않았습니다. 트렁크와 브랜치 사용 정책을 정하느냐에 따라서 다르지만, 어떤 프로젝트에서는 트렁크에는 아래와 같이 머지를 했다는 기록 밖에 없고, 머지를 담당한 사람의 이름밖에는 남아 있지 않았었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://camo.githubusercontent.com/abca6698c82e72666dac32691aa356562e04452c/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f73766e5f686973746f72792e706e67&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://camo.githubusercontent.com/abca6698c82e72666dac32691aa356562e04452c/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f73766e5f686973746f72792e706e67&quot; alt=&quot;SVN history&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git에서는 원한다면 모든 이력을 남길 수 있습니다. Eclipse plugin으로 보니 머지한 이력을 그래프로 이쁘게 보여줬습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://camo.githubusercontent.com/2726e7ce963eff68da29490e80bb00880ec774cc/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f6769745f686973746f72792e706e67&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://camo.githubusercontent.com/2726e7ce963eff68da29490e80bb00880ec774cc/687474703a2f2f646c2e64726f70626f782e636f6d2f752f31333936303330302f6769742f6769745f686973746f72792e706e67&quot; alt=&quot;Git history&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git에서는 커밋 로그를 정교하게 수정할 수도 있습니다. 몇개의 커밋을 하나로 합칠 수도 있고, 앞에 커밋한 이력을 수정할 수도 있습니다.파일을 하나 빠뜨리고 커밋을 해서 불필요하게 커밋이 나누어지거나, 설명을 대충 적은 것 같아서 아쉬울 때가 많았는데 Git에서는 commit을 한 이후에도 그걸 보완하는 작업이 가능합니다. 어쩌면 한 번에 완벽한 코드를 짜는 것이 불가능한 것처럼, 한번에 깔끔한 커밋 이력을 남기고 논리적으로 균일한 커밋 단위를 관리하기는 어렵습니다.오래 가는 코드를 위해서는 커밋 로그는 깔끔하고 친절하게 정리되어야 합니다. Git을 쓰면 commit 로그도 리팩토링의 대상이 되어서 더 정교한 commit 관리를 원하는 사람에게 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437eab7b8eba6aceab3a0_gitflow_gerrit그리고_gitflow_gerrit&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EA%B7%B8%EB%A6%AC%EA%B3%A0-gitflow-gerrit&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EA%B7%B8%EB%A6%AC%EA%B3%A0-gitflow-gerrit&lt;/a&gt;그리고 Gitflow, Gerrit&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금까지는 직접 경험한 장점을 이야기했는데, &lt;a href=&quot;https://github.com/nvie/gitflow&quot;&gt;Gitflow&lt;/a&gt;와 &lt;a href=&quot;http://code.google.com/p/gerrit/&quot;&gt;Gerrit&lt;/a&gt;은 직접 써본 도구는 아닙니다. 그래도 Git의 매력중의 중요한 부분이라고 생각되어서 간단하게 언급하고 넘어갑니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nvie/gitflow&quot;&gt;Gitflow&lt;/a&gt;는 Best Practice라고 할만한 버전관리 절차를 Git으로 편하게 쓸 수 있게 도와주는 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://camo.githubusercontent.com/f862a6cc2cf265c31f354a666120ce26c224db8c/687474703a2f2f6e7669652e636f6d2f696d672f323030392f31322f53637265656e2d73686f742d323030392d31322d32342d61742d31312e33322e30332e706e67&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://camo.githubusercontent.com/f862a6cc2cf265c31f354a666120ce26c224db8c/687474703a2f2f6e7669652e636f6d2f696d672f323030392f31322f53637265656e2d73686f742d323030392d31322d32342d61742d31312e33322e30332e706e67&quot; alt=&quot;Gitflow model&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git이 워낙 기능이 많기 때문에 어떤 버전관리 정책을 써야할지 처음에는 막막하게 느껴질 수도 있을 것 같습니다. GitFlow는 &apos;Feature&apos; ,&apos;Hotfix&apos; ,&apos;Release&apos; 등과 같이 전형적인 역할의 브랜치들을 기본적으로 녹여내고 있습니다. 즉 좀 더 정형화되고 추상화된 개념들을 더 짧은 명령어로 쓸 수 있는 것입니다. &apos;git flow hotfix start&amp;#8217;와 같이 명령어를 치면, hotfix를 위한 branch 생성을 해주는 식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://code.google.com/p/gerrit/&quot;&gt;Gerrit&lt;/a&gt;은 Git바탕의 코드 리뷰도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 블로거는 &lt;a href=&quot;http://alblue.bandlem.com/2011/02/someday.html&quot;&gt;Getrrit을 소개하면서 &apos;Someday, all software will be built this way.&apos;&lt;/a&gt;라고 주장하기도 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래에 Gerrit과 Jenkins를 연동한 인상적인 Demo도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://alblue.bandlem.com/2011/02/gerrit-git-review-with-jenkins-ci.html&quot; class=&quot;bare&quot;&gt;http://alblue.bandlem.com/2011/02/gerrit-git-review-with-jenkins-ci.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;9분 40초부터 1분동안의 데모가 핵심적인 장면입니다. 코드를 수정하고 commit, push를 하고 Gerrit에 들어가보면 CI서버지인 Jenkins에서는 &apos;verified&amp;#8217;되었다는 표시가 나옵니다.그리고 그 코드를 리뷰해서 &apos;Great&amp;#8217;라는 메시지를 달아주고 +2점으로 점수를 부여해주는 장면이 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;httpsgist_github_combenelog2922437eb85bcec9f81eab1b0eba6aceba5bc논쟁거리를&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%85%BC%EC%9F%81%EA%B1%B0%EB%A6%AC%EB%A5%BC&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%85%BC%EC%9F%81%EA%B1%B0%EB%A6%AC%EB%A5%BC&lt;/a&gt;논쟁거리를&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞서서 Git을 쓰면 통합을 자주 안 하게 된다는 주장에 대해서 말씀드렸지만, 그외에도 Git에 대한 이런저런 불평들은 많이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect4&quot;&gt;
&lt;h5 id=&quot;httpsgist_github_combenelog2922437ec96b4eba0a4ec9b8cec9a94_ebb3b5ec9ea1ed95b4ec9a94어려워요_복잡해요&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EC%96%B4%EB%A0%A4%EC%9B%8C%EC%9A%94-%EB%B3%B5%EC%9E%A1%ED%95%B4%EC%9A%94&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EC%96%B4%EB%A0%A4%EC%9B%8C%EC%9A%94-%EB%B3%B5%EC%9E%A1%ED%95%B4%EC%9A%94&lt;/a&gt;어려워요~ 복잡해요~&lt;/h5&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우선 기능이 많고 어렵다는 것입니다. 이 부분은 저도 어느 정도 동감을 합니다.저도 프로젝트를 끝낸 후에 오랜 만에 다시 Git를 쓰려니 명령어가 잘 기억이 나지 않아서 헤메었던 기억이 있습니다.그리고 분명히 SVN보다 더 많은 개념과 기능을 제공하니 제대로 쓰려면 배워야할 것이 많기도 합니다. 중간에 많은 시행착오를 거치거나 실수로 소스를 날려먹은 사람도 몇번 봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇지만 처음부터 그 많은 기능을 모두 알 필요는 없다고 생각합니다.간단히 SVN으로 하던 주요 사용법만 Git으로 배우는 것은 얼마 걸리지 않습니다.그리고 차근차근 기능을 익혀나가면 Git으로 더 편리하게 할 수 있는 일들이 늘어가고 앞에서 언급한 Git의 장점로 개발이 더 편해질 것이라 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Git의 많은 기능들은 Git 자체의 오버엔지니어링이라기보다는, 버전관리 업무 자체의 복잡함을 보여준다고 생각합니다.수많은 작업자가 같이 작업하고, 여러 배포 버전을 동시에 살려나가야 하는 대규모 소프트웨어는 버전관리 자체가 워낙 어려운 과제입니다.그런 문제 상황을 정교하게 분류하다보니, 점점 새로운 개념들이 생겨나서 처음 접하는 사람에게는 논리적으로 어렵다고 느껴질 듯합니다.그런데 그런 개념을 어떻게 응용할지 고민하면서 쓰다보면, 지금까지의 버전관리가 충분하지 않았음을 느끼게 됩니다.저도 파일 공유 서버로 버전관리가 충분했다고 생각하는 시절에는 SVN에서 제공하는 transactional한 commit 같은 기능이 필요하다고 생각하지 못했었습니다.마찬가지로 DVCS가 없이도 큰 아쉬움은 없었는데, Git등을 써보고 나니 이전에는 SVN에서 그냥 넘어갔던 부분이 더 불편하게 느껴졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect4&quot;&gt;
&lt;h5 id=&quot;httpsgist_github_combenelog2922437eclipse_pluginec9db4_ebb688ed8eb8ed95b4ec9a94eclipse_plugin이_불편해요&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#eclipse-plugin%EC%9D%B4-%EB%B6%88%ED%8E%B8%ED%95%B4%EC%9A%94&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#eclipse-plugin%EC%9D%B4-%EB%B6%88%ED%8E%B8%ED%95%B4%EC%9A%94&lt;/a&gt;Eclipse Plugin이 불편해요&lt;/h5&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또 하나는 Eclipse plugin이 아직도 불안정하고 불편하다는 점입니다. 저는 Eclipse plugin은 history를 보는 용도로만 쓰고 모든 작업은 명령행으로 해서 특별히 불편을 겪은 적은 없습니다.다른 써보신 분들에 의견에 따르면 Eclipse plugin만으로는 Git의 모든 기능을 원활히 쓸 수 없다고 합니다.그래서 Eclipse plugin은 덤정도로 여기면서 큰 기대를 안 하고, 명령행 방식을 주로 쓴다고 생각하면 오히려 시행착오가 적을 것 같기도 합니다.명령어를 외우는 것이 처음에는 부담될지 몰라도, 나중에는 메뉴를 찾아해메는 것보다 작업속도가 더 빨리질수도 있습니다. 그리고 정보공유나 인수인계에서는 GUI보다 명령행이 더 유리하기도 하고, 더 재미있어할 개발자들도 많을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;httpsgist_github_combenelog2922437eba788ecb998eba9b0마치며&quot;&gt;&lt;a href=&quot;https://gist.github.com/benelog/2922437#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/2922437#%EB%A7%88%EC%B9%98%EB%A9%B0&lt;/a&gt;마치며&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버전관리는 새로운 시도를 하기에는 워낙 두려운 분야이기는 합니다. 현재 방식의 버전관리로 충분히 만족하는 조직이 많고, 조직의 관습이나 변화에 필요한 비용등을 생각하면 그것이 어떤 조직에서는 정답일수도 있습니다.저도 돌이켜보면 버전관리의 필요성을 못 느끼던 시절부터 시작해서, Merant Version manager, SVN, Git을 거쳤고, 그 사이에 생각도 많이 바뀌었습니다.원래 하던 방식이 마음이 편했기에 제가 처한 환경의 특수성을 과대평가해서 새로운 방식이 필요하지 않다고 생각했던 시절이 많았었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래도 발전한 도구들 덕분에 개발은 훨씬 편해진 것 같기도 하지만, 더 어려워지기도 했습니다. 단순히 소프트웨어 개발만 할 줄 알던 시절에서 빌드도구, 버전관리 도구에 대해서도 전에보다 더 많은 지식을 알아야 코드 한 줄을 쓸 수 있게 되었습니다.어떤 분들은 이런 흐름이 필요이상의 복잡함을 더했다고 느끼겠고, 새로 업계에 들어오는 개발자들에게는 점점 숙제거리가 늘어만 가는듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 이런 변화는 발전이고, 여러 조직에서 비슷한 고민들이 있어서 공유하다가 약간씩 더 나은 해결방식이 나오고 있다고 봅니다. 저는 Git을 쓰는 프로젝트를 처음 해 보면서 다른 개발자들이 어떤 고민을 했을지 좀 더 많이 이해했고, 그런 고민들을 저는 지나쳤음을 깨달았습니다.그 점이 Git으로 얻은 가장 큰 선물이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>스레드 안전성의 문서화와 검증: Javadoc 표기, 애너테이션, 정적 분석, ArchUnit</title>
      <link>https://blog.benelog.net//thread-safety-annotations.html</link>
      <pubDate>Wed, 30 May 2012 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">thread-safety-annotations.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#javadoc으로_표시하는_스레드_안전성&quot;&gt;Javadoc으로 표시하는 스레드 안전성&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#javadoc이_synchronized를_감추는_이유&quot;&gt;Javadoc이 synchronized를 감추는 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#클래스_수준_스레드_안전성_표기의_필요성&quot;&gt;클래스 수준 스레드 안전성 표기의 필요성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#effective_java의_스레드_안전성_다섯_단계&quot;&gt;Effective Java의 스레드 안전성 다섯 단계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#클래스_설명문의_표기_사례&quot;&gt;클래스 설명문의 표기 사례&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#애너테이션으로_표시하는_스레드_안전성&quot;&gt;애너테이션으로 표시하는 스레드 안전성&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#apache_httpclient의_contract&quot;&gt;Apache HttpClient의 @Contract&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jcip_애너테이션&quot;&gt;JCIP 애너테이션&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jcip와_같은_이름으로_퍼진_애너테이션들&quot;&gt;JCIP와 같은 이름으로 퍼진 애너테이션들&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#목적에_맞는_애너테이션_고르기&quot;&gt;목적에 맞는 애너테이션 고르기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정적_분석_도구의_검사_범위&quot;&gt;정적 분석 도구의 검사 범위&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#spotbugs&quot;&gt;SpotBugs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#error_prone&quot;&gt;Error Prone&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#intellij_idea&quot;&gt;IntelliJ IDEA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#archunit으로_검증하는_프로젝트별_규칙&quot;&gt;ArchUnit으로 검증하는 프로젝트별 규칙&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#예제_코드&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#규칙과_실행_결과&quot;&gt;규칙과 실행 결과&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어떤 클래스의 인스턴스를 여러 스레드가 공유해도 되는지는 개발할 때 주의 깊게 살펴야 할 정보입니다. 그런데 Javadoc의 표준 태그(&lt;code&gt;@param&lt;/code&gt;, &lt;code&gt;@return&lt;/code&gt;, &lt;code&gt;@throws&lt;/code&gt; 등)에는 스레드 안전성을 표시하는 태그가 없습니다. 그래서 클래스 사용자가 제공자의 의도를 놓치기 쉽습니다.
이 글은 Javadoc에 스레드 안전성을 명시할 때 바람직한 분류 방식을 먼저 살펴봅니다. 이어서 애너테이션, 정적 분석 도구, ArchUnit 테스트로 멀티스레드에서 위험한 코드가 배포되지 않도록 방어하는 방법을 정리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;javadoc으로_표시하는_스레드_안전성&quot;&gt;Javadoc으로 표시하는 스레드 안전성&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 절에서는 다음 네 가지를 차례로 살펴봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Javadoc이 메서드의 &lt;code&gt;synchronized&lt;/code&gt;를 표시하지 않는 이유&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그럼에도 클래스 수준의 표기가 필요한 이유&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;문서화할 때 적절한 분류&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;실제 라이브러리 문서의 사례&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;javadoc이_synchronized를_감추는_이유&quot;&gt;Javadoc이 synchronized를 감추는 이유&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;클래스의 인스턴스 메서드에 &lt;code&gt;synchronized&lt;/code&gt; 키워드가 붙으면 그 인스턴스 자체가 lock이 됩니다. 즉 여러 스레드가 한 인스턴스의 &lt;code&gt;synchronized&lt;/code&gt; 메서드를 동시에 호출해도 한 스레드만 진입할 수 있습니다. 다른 스레드는 먼저 들어간 스레드가 메서드를 마칠 때까지 기다립니다. &lt;code&gt;static synchronized&lt;/code&gt; 메서드는 해당 클래스의 &lt;code&gt;Class&lt;/code&gt; 객체를 lock으로 사용합니다. 따라서 &lt;code&gt;synchronized&lt;/code&gt;는 스레드 안전성을 판단하는 단서가 되고, Javadoc에 공개되면 도움이 될 만한 정보입니다.
그러나 Javadoc은 메서드 선언의 &lt;code&gt;synchronized&lt;/code&gt; 키워드를 출력하지 않습니다. 동기화 여부는 구현 세부 사항이라서 API 문서에 노출할 계약이 아니라고 보기 때문입니다. Effective Java 3판의 아이템 82도 같은 근거를 들어 이 정책을 옹호합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드 선언부의 &lt;code&gt;synchronized&lt;/code&gt;는 메서드 본문 전체를 &lt;code&gt;synchronized(this)&lt;/code&gt; 블록으로 감싼 것과 같습니다. 즉 아래 코드는&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;메서드 선언부의 synchronized&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;synchronized void run() {
    // do something
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음 코드와 같은 일을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;메서드 내부 전체를 synchronized 블록으로 선언&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;void run() {
    synchronized (this) {
        // do something
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구현을 개선하면서 동기화 구간을 메서드의 일부로 좁힐 수도 있고, &lt;code&gt;this&lt;/code&gt; 대신 별도의 lock 객체를 쓸 수도 있습니다. 이런 세부 구현은 외부 인터페이스보다 자주 바뀝니다. lock으로 보호되는 모든 구간을 메서드 선언부에 표시하기도 어렵습니다. 내부적으로 &lt;code&gt;java.util.concurrent.locks.Lock&lt;/code&gt;이나 CAS(Compare And Swap) 연산으로 동기화한 클래스도 있습니다. 따라서 &lt;code&gt;synchronized&lt;/code&gt; 키워드의 유무는 스레드 안전성을 판단하는 유일한 기준이 되지 못합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;클래스_수준_스레드_안전성_표기의_필요성&quot;&gt;클래스 수준 스레드 안전성 표기의 필요성&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;멀티스레드 환경에서는 메서드 하나씩 호출할 때는 안전하더라도 여러 메서드 호출을 조합하면 안전하지 않을 수 있습니다.
예를 들어 &lt;code&gt;HashMap&lt;/code&gt;은 생성이 끝난 뒤 안전하게 공개되고(예: &lt;code&gt;final&lt;/code&gt; 필드나 &lt;code&gt;volatile&lt;/code&gt; 변수로 전달) 더 이상 변경되지 않는다면 여러 스레드가 &lt;code&gt;get()&lt;/code&gt;을 호출해도 문제가 없습니다. 그러나 한 스레드가 &lt;code&gt;put()&lt;/code&gt;으로 구조를 바꾸는 동안 다른 스레드가 &lt;code&gt;get()&lt;/code&gt;을 호출하면 결과를 보장할 수 없습니다. public 메서드가 모두 &lt;code&gt;synchronized&lt;/code&gt;이거나 동기화된 메서드·뷰에 위임하는 &lt;code&gt;Hashtable&lt;/code&gt;이나 &lt;code&gt;Collections.synchronizedMap()&lt;/code&gt;으로 감싼 &lt;code&gt;Map&lt;/code&gt;도 마찬가지입니다. &lt;code&gt;containsKey()&lt;/code&gt;로 확인한 뒤 &lt;code&gt;put()&lt;/code&gt;하는 것처럼 호출 두 개를 조합할 때 외부에서 lock을 잡지 않으면 경쟁 조건이 생깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러므로 문서화할 대상은 메서드의 동기화 여부가 아니라 클래스가 어떤 조건에서 안전한지입니다. Javadoc에 이를 표시할 규약이 없으니 개발자가 클래스 설명에 직접 적어야 합니다. 다음 절에서 볼 Effective Java의 분류가 그 기준이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;effective_java의_스레드_안전성_다섯_단계&quot;&gt;Effective Java의 스레드 안전성 다섯 단계&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Effective Java 3판의 아이템 82 &apos;스레드 안전성 수준을 문서화하라&apos;(2판에서는 아이템 70)는 스레드 안전성을 다섯 단계로 나눠 문서화하라고 권합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;불변(immutable)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;상태가 바뀌지 않으므로 외부 동기화가 필요 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예: &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Long&lt;/code&gt;, &lt;code&gt;BigInteger&lt;/code&gt;(책의 예), &lt;code&gt;java.time.LocalDate&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;무조건적 스레드 안전(unconditionally thread-safe)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;상태가 있으나 내부에서 충분히 동기화합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예: &lt;code&gt;AtomicLong&lt;/code&gt;, &lt;code&gt;ConcurrentHashMap&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;조건부 스레드 안전(conditionally thread-safe)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;일부 메서드는 외부 동기화가 있어야 안전합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예: &lt;code&gt;Collections.synchronizedList()&lt;/code&gt;로 감싼 &lt;code&gt;List&lt;/code&gt;. iterator로 순회하는 동안에는 &lt;code&gt;List&lt;/code&gt; 객체를 lock으로 잡아야 합니다. 그렇지 않으면 동작을 예측할 수 없습니다(non-deterministic behavior). fail-fast iterator가 &lt;code&gt;ConcurrentModificationException&lt;/code&gt;을 던질 수도 있지만, 반드시 던진다는 보장은 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스레드 안전하지 않음(not thread-safe)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;외부에서 동기화해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예: &lt;code&gt;HashMap&lt;/code&gt;, &lt;code&gt;ArrayList&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스레드 적대적(thread-hostile)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;외부에서 동기화해도 멀티스레드에서 쓸 수 없습니다. 동기화 없이 static 데이터를 수정하는 클래스나 메서드가 여기에 해당합니다. 호출하는 쪽이 각자 다른 인스턴스를 lock으로 잡아도 같은 static 데이터를 동시에 바꾸게 되기 때문입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예: 3판은 아이템 78의 &lt;code&gt;generateSerialNumber&lt;/code&gt; 메서드가 내부 동기화 없이 static 필드를 증가시킨다면 스레드 적대적이라고 설명합니다. 2판이 예로 든 &lt;code&gt;System.runFinalizersOnExit()&lt;/code&gt;는 JDK 1.2에서 폐기(deprecated)되었고 JDK 11에서 제거되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;클래스_설명문의_표기_사례&quot;&gt;클래스 설명문의 표기 사례&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 표준 라이브러리와 Spring Batch가 스레드 안전성을 어디에 어떻게 기술하는지 세 가지 예를 보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;java_util_linkedlist&quot;&gt;java.util.LinkedList&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedList.html&quot;&gt;LinkedList의 JDK 25 Javadoc&lt;/a&gt;은 클래스 설명의 세 번째 문단에서 굵은 글씨로 &apos;not synchronized&amp;#8217;라고 밝힙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/thread-safety-annotations/LinkedList-doc.png&quot; alt=&quot;LinkedList의 JDK 25 Javadoc&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;java_text_simpledateformat&quot;&gt;java.text.SimpleDateFormat&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/text/SimpleDateFormat.html&quot;&gt;SimpleDateFormat의 JDK 25 Javadoc&lt;/a&gt;은 클래스 설명의 마지막 절로 &apos;Synchronization&amp;#8217;이라는 제목을 두고 &apos;Date formats are not synchronized&amp;#8217;라고 설명합니다. &apos;API Note&amp;#8217;에서는 &lt;code&gt;DateTimeFormatter&lt;/code&gt;를 &apos;immutable and thread-safe alternative&amp;#8217;로 권합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/thread-safety-annotations/SimpleDateFormat-doc.png&quot; alt=&quot;SimpleDateFormat의 JDK 25 Javadoc&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/format/DateTimeFormatter.html&quot;&gt;DateTimeFormatter의 JDK 25 Javadoc&lt;/a&gt;은 클래스 설명 끝의 &apos;Implementation Requirements&apos; 항목에 &apos;This class is immutable and thread-safe.&apos;라고 명시합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/thread-safety-annotations/DateTimeFormatter-doc.png&quot; alt=&quot;DateTimeFormatter의 JDK 25 Javadoc&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 8에서 추가된 &lt;code&gt;java.time&lt;/code&gt; 패키지는 이 형식을 패키지 전체에 맞췄습니다. JDK 25 소스 기준으로 &lt;code&gt;java.time&lt;/code&gt;과 하위 패키지의 public 클래스 43개 중 &lt;code&gt;LocalDate&lt;/code&gt;, &lt;code&gt;Instant&lt;/code&gt;, &lt;code&gt;ZonedDateTime&lt;/code&gt; 등 31개가 Javadoc의 &lt;code&gt;@implSpec&lt;/code&gt; 태그로 같은 문장을 같은 자리에 명시합니다. public enum 12개도 모두 &apos;This is an immutable and thread-safe enum.&apos;처럼 같은 틀의 문장을 씁니다. 나머지 클래스 12개도 유틸리티 클래스 &lt;code&gt;TemporalQueries&lt;/code&gt; 하나를 빼면 같은 항목에서 불변 여부나 스레드 사용 조건을 밝힙니다. 예를 들어 예외 클래스는 &apos;This class is intended for use in a single thread.&apos;라고 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 이 형식이 JDK 전체의 표준으로 굳어졌다고 보기는 어렵습니다.
&lt;code&gt;@implSpec&lt;/code&gt;은 Javadoc 표준 태그가 아니라 JDK를 빌드할 때 &lt;code&gt;-tag&lt;/code&gt; 옵션으로 등록하는 JDK 전용 태그입니다. 일반 프로젝트에서 그대로 쓰면 JDK 25의 &lt;code&gt;javadoc&lt;/code&gt; 명령이 &apos;unknown tag. Unregistered custom tag?&apos; 오류를 냅니다.
JDK 안에서도 새 API마다 방식이 다릅니다. JDK 22에서 정식 API가 된 &lt;code&gt;java.lang.foreign&lt;/code&gt;의 &lt;code&gt;Arena&lt;/code&gt;, &lt;code&gt;Linker&lt;/code&gt;와 JDK 22에 추가된 &lt;code&gt;ListFormat&lt;/code&gt;은 &lt;code&gt;@implSpec&lt;/code&gt;에 스레드 안전성을 명시합니다. 반면 JDK 17에 추가된 &lt;code&gt;HexFormat&lt;/code&gt;은 같은 &apos;This class is immutable and thread-safe.&apos; 문장을 &lt;code&gt;@implSpec&lt;/code&gt; 없이 본문에 쓰고, JDK 11에 추가된 &lt;code&gt;HttpClient&lt;/code&gt;도 본문에서 &apos;Once built, an HttpClient is immutable&amp;#8217;이라고 설명합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;org_springframework_batch_item_file_flatfileitemwriter&quot;&gt;org.springframework.batch.item.file.FlatFileItemWriter&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Batch 5.2.6의 &lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/file/FlatFileItemWriter.html&quot;&gt;FlatFileItemWriter&lt;/a&gt; Javadoc은 클래스 설명의 마지막 줄에 &apos;not&amp;#8217;만 굵게 표시해서 &apos;The implementation is &lt;strong&gt;not&lt;/strong&gt; thread-safe.&apos;라고 밝힙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/thread-safety-annotations/FlatFileItemWriter-doc.png&quot; alt=&quot;FlatFileItemWriter의 Spring Batch 5.2.6 Javadoc&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 클래스마다 스레드 안전성을 표시하는 위치와 방법이 제각각입니다. &lt;code&gt;LinkedList&lt;/code&gt;와 &lt;code&gt;FlatFileItemWriter&lt;/code&gt;는 굵은 글씨로, &lt;code&gt;SimpleDateFormat&lt;/code&gt;은 별도 절 제목으로 강조합니다. 그러나 앞의 네 클래스 모두 스레드 안전성을 클래스 설명의 첫 문단이 아니라 중간이나 끝에 적어서, API 문서를 끝까지 읽는 사람이 아니라면 지나치기 쉽습니다. &apos;스레드 안전성은 클래스 설명의 제일 윗줄에 넣고, 반드시 눈에 띄게 표시한다&apos; 같은 규칙이 있었으면 얼마나 좋을까 하는 생각까지 듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;애너테이션으로_표시하는_스레드_안전성&quot;&gt;애너테이션으로 표시하는 스레드 안전성&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설명문 대신 애너테이션으로 스레드 안전성을 표시하면 Javadoc에 나타나는 위치와 형식이 일정해집니다. 애너테이션 선언에 &lt;code&gt;@Documented&lt;/code&gt; 메타 애너테이션이 붙어 있으면 Javadoc이 클래스 선언부에 그 애너테이션을 그대로 출력하기 때문입니다.
한 프로젝트가 그런 용도의 애너테이션을 직접 정의한 사례를 먼저 보고, 여러 프로젝트가 같이 쓸 수 있는 애너테이션을 이어서 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;apache_httpclient의_contract&quot;&gt;Apache HttpClient의 @Contract&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://hc.apache.org/httpcomponents-client-5.5.x/&quot;&gt;Apache HttpClient&lt;/a&gt; 5는 스레드 안전성을 표시하는 애너테이션을 &lt;code&gt;org.apache.hc.core5.annotation.Contract&lt;/code&gt; 하나로 정의했습니다. &lt;code&gt;threading&lt;/code&gt; 속성에 &lt;a href=&quot;https://hc.apache.org/httpcomponents-core-5.3.x/current/httpcore5/apidocs/org/apache/hc/core5/annotation/ThreadingBehavior.html&quot;&gt;ThreadingBehavior&lt;/a&gt; enum 값을 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 75%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;ThreadingBehavior&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IMMUTABLE&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;완전한 불변 객체이며 스레드 안전합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IMMUTABLE_CONDITIONAL&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;생성자로 주입받은 의존 객체가 불변일 때 불변이고, 의존 객체가 스레드 안전할 때 스레드 안전합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;STATELESS&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;상태가 없으며 스레드 안전합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;SAFE&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;스레드 안전합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;SAFE_CONDITIONAL&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;생성자로 주입받은 의존 객체가 스레드 안전할 때만 스레드 안전합니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;UNSAFE&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;스레드 안전하지 않습니다. &lt;code&gt;threading&lt;/code&gt; 속성을 생략했을 때의 기본값입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HttpClient 5.5의 소스에서 주요 클래스들은 아래처럼 선언되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;HttpClient 5.5&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Contract(threading = ThreadingBehavior.SAFE)
public abstract class CloseableHttpClient implements HttpClient, ModalCloseable {

@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)
public class PoolingHttpClientConnectionManager
        implements HttpClientConnectionManager, ConnPoolControl&amp;lt;HttpRoute&amp;gt; {

@Contract(threading = ThreadingBehavior.SAFE)
public class BasicHttpClientConnectionManager implements HttpClientConnectionManager {&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;SAFE_CONDITIONAL&lt;/code&gt;은 Effective Java의 &apos;조건부 스레드 안전&amp;#8217;과 이름은 비슷하지만 뜻이 다릅니다. Effective Java의 분류는 &apos;어떤 메서드 호출 순서에 외부 동기화가 필요한가&amp;#8217;를 말하고, HttpClient의 값은 &apos;생성자로 받은 의존 객체가 스레드 안전한가&amp;#8217;를 말합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@Contract&lt;/code&gt;에는 &lt;code&gt;@Documented&lt;/code&gt; 메타 애너테이션이 붙어 있어서 Javadoc의 클래스 선언부에 함께 나타납니다. 아래는 &lt;a href=&quot;https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/org/apache/hc/client5/http/impl/io/BasicHttpClientConnectionManager.html&quot;&gt;BasicHttpClientConnectionManager의 Javadoc&lt;/a&gt;입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/thread-safety-annotations/BasicHttpClientConnectionManager-doc.png&quot; alt=&quot;BasicHttpClientConnectionManager의 HttpClient 5 Javadoc&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일관된 위치에 표시되기 때문에 한눈에 스레드 안전성을 알아볼 수 있습니다. 클래스 설명 문단에도 &apos;this class is fully thread-safe&amp;#8217;라고 적혀 있지만, 선언부의 애너테이션이 먼저 눈에 들어옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HttpClient가 처음부터 독자적인 애너테이션을 만든 것은 아닙니다. &lt;a href=&quot;https://github.com/apache/httpcomponents-client/tree/rel/v4.5.2/httpclient/src/main/java/org/apache/http&quot;&gt;HttpClient 4.5.2&lt;/a&gt;까지는 &lt;code&gt;HttpGet&lt;/code&gt;과, 위 5.5 예제에 나온 클래스들의 선언에 아래처럼 &lt;code&gt;@NotThreadSafe&lt;/code&gt;, &lt;code&gt;@ThreadSafe&lt;/code&gt;가 붙어 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;HttpClient 4.5.2&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@NotThreadSafe
public class HttpGet extends HttpRequestBase {

@ThreadSafe
public abstract class CloseableHttpClient implements HttpClient, Closeable {

@ThreadSafe
public class PoolingHttpClientConnectionManager
    implements HttpClientConnectionManager, ConnPoolControl&amp;lt;HttpRoute&amp;gt;, Closeable {

@ThreadSafe
public class BasicHttpClientConnectionManager implements HttpClientConnectionManager, Closeable {&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 애너테이션들은 &lt;code&gt;org.apache.http.annotation&lt;/code&gt; 패키지에 들어 있었지만, Javadoc은 &apos;Java Concurrency in Practice&apos; 책에서 유래했다고 설명했습니다. 그런데 뒤에서 볼 JCIP 원본 라이브러리의 &lt;a href=&quot;https://issues.apache.org/jira/browse/HTTPCLIENT-1743&quot;&gt;라이선스 문제&lt;/a&gt; 때문에 HttpCore 4.4.5는 이 네 가지 애너테이션을 제거했고, HttpClient도 4.5.3부터 &lt;code&gt;@Contract&lt;/code&gt;를 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jcip_애너테이션&quot;&gt;JCIP 애너테이션&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HttpClient처럼 프로젝트마다 애너테이션을 정의해도 됩니다. 그러나 널리 쓰이는 오픈소스의 애너테이션을 쓰면 사용자가 새로 익힐 필요가 없습니다. 뒤에서 볼 SpotBugs와 IntelliJ IDEA도 인식하는 애너테이션 목록에 JCIP 패키지를 기본으로 넣어 두었습니다. HttpClient 4.x가 가져다 쓴 JCIP 애너테이션이 그런 공통 애너테이션의 출발점입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://jcip.net/annotations/doc/net/jcip/annotations/package-summary.html&quot;&gt;JCIP 애너테이션&lt;/a&gt;은 &apos;Java Concurrency in Practice&apos; 책의 부록 A에서 제안한, 스레드 안전성을 표시하는 애너테이션으로 아래 네 가지가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@ThreadSafe&lt;/code&gt; : 스레드 안전한 클래스&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@NotThreadSafe&lt;/code&gt; : 스레드 안전하지 않은 클래스&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@Immutable&lt;/code&gt; : 불변 클래스. 불변이면 스레드 안전합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@GuardedBy(&quot;lock&quot;)&lt;/code&gt; : 필드나 메서드에 붙여서, 어떤 lock을 잡은 상태에서만 접근해야 하는지 표시합니다. &lt;code&gt;synchronized&lt;/code&gt;에 쓰는 내장 lock과 &lt;code&gt;java.util.concurrent.locks.Lock&lt;/code&gt;을 모두 지정할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 세 애너테이션은 클래스의 계약을 사용자에게 알리고, &lt;code&gt;@GuardedBy&lt;/code&gt;는 해당 클래스를 유지보수하는 사람에게 어떤 lock을 지켜야 하는지 보여 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Effective Java 2판(2008년)은 JCIP(2006년)보다 늦게 나왔고, 다섯 단계를 JCIP 애너테이션과 비교해 설명합니다.
두 책의 분류를 맞춰 보면 스레드 적대적을 뺀 네 단계가 JCIP의 클래스용 애너테이션 세 개에 대응합니다. 무조건적 스레드 안전과 조건부 스레드 안전은 &lt;code&gt;@ThreadSafe&lt;/code&gt; 하나로 묶입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 50%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Effective Java의 다섯 단계&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;JCIP 애너테이션&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;차이&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;불변&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;@Immutable&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JCIP는 불변이면 스레드 안전하다고 보므로 &lt;code&gt;@ThreadSafe&lt;/code&gt;를 겹쳐 붙이지 않습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;무조건적 스레드 안전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;@ThreadSafe&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;같은 뜻입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;조건부 스레드 안전&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;@ThreadSafe&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JCIP에는 별도의 이름이 없습니다. 어떤 lock을 잡아야 하는지는 설명문에 적습니다. 두 분류의 차이가 실제로 드러나는 항목입니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;스레드 안전하지 않음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;@NotThreadSafe&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JCIP는 이 애너테이션을 선택 사항으로 둡니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;스레드 적대적&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JCIP에는 대응하는 개념이 없습니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;해당 없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;@GuardedBy(&quot;lock&quot;)&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;필드와 메서드에 붙이며 유지보수하는 사람을 위한 정보입니다. Effective Java의 다섯 단계는 클래스 단위로 사용자와 맺는 계약이므로 분류의 기준 자체가 다릅니다.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하면 Effective Java는 사용자 관점에서 외부 동기화가 얼마나 필요한지를 다섯 단계로 나눈 분류입니다. JCIP는 안전한지 아닌지의 이분법에 불변을 특수한 경우로 추가하고, 유지보수하는 사람을 위한 정보는 &lt;code&gt;@GuardedBy&lt;/code&gt;로 따로 분리했습니다. JCIP 4.5절 &apos;Documenting synchronization policies&amp;#8217;는 스레드 안전성 보장은 사용자를 위해, 동기화 정책은 유지보수하는 사람을 위해 문서화하라고 권합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jcip와_같은_이름으로_퍼진_애너테이션들&quot;&gt;JCIP와 같은 이름으로 퍼진 애너테이션들&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JCIP 책과 함께 배포된 원본 라이브러리는 Maven Central의 &lt;code&gt;net.jcip:jcip-annotations:1.0&lt;/code&gt;입니다. 그런데 이 라이브러리의 라이선스는 Creative Commons Attribution입니다. Creative Commons 재단 스스로 소프트웨어에는 권하지 않는 라이선스입니다. 그래서 같은 API를 Apache License 2.0으로 다시 구현한 &lt;a href=&quot;https://github.com/stephenc/jcip-annotations&quot;&gt;com.github.stephenc.jcip:jcip-annotations:1.0-1&lt;/a&gt;이 나왔습니다. 두 라이브러리는 패키지 이름(&lt;code&gt;net.jcip.annotations&lt;/code&gt;)과 애너테이션 이름이 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 외에도 JCIP 애너테이션은 여러 프로젝트로 복제되었습니다. 다음 라이브러리들은 애너테이션 이름은 같지만 패키지가 다릅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;javax.annotation.concurrent&lt;/code&gt; : FindBugs가 배포한 JSR-305 애너테이션 구현인 &lt;code&gt;com.google.code.findbugs:jsr305&lt;/code&gt;에 같은 네 가지 애너테이션이 들어 있습니다. &lt;a href=&quot;https://jcp.org/en/jsr/proposalDetails?id=305&quot;&gt;JSR-305 자체는 정식 릴리스 없이 중단되어 dormant 상태&lt;/a&gt;입니다. Java 9와 10에서는 module path에 올린 이 jar의 &lt;code&gt;javax.annotation&lt;/code&gt; 패키지가 JDK의 &lt;code&gt;java.xml.ws.annotation&lt;/code&gt; 모듈과 겹칠 수 있었습니다. 다만 이 JDK 모듈은 &lt;a href=&quot;https://docs.oracle.com/en/java/javase/26/migrate/removed-tools-components.html&quot;&gt;JDK 11에서 제거&lt;/a&gt;되었으므로, Java 9 이후 모든 버전에 해당하는 문제는 아닙니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;com.google.errorprone.annotations&lt;/code&gt; : Google의 Error Prone이 쓰는 &lt;code&gt;@Immutable&lt;/code&gt;, &lt;code&gt;@ThreadSafe&lt;/code&gt;와 &lt;code&gt;concurrent&lt;/code&gt; 하위 패키지의 &lt;code&gt;@GuardedBy&lt;/code&gt;가 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;androidx.annotation.GuardedBy&lt;/code&gt; : Android용입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;org.apache.http.annotation&lt;/code&gt; : 앞에서 본 대로 Apache HttpComponents가 4.5.2까지 복제해 쓰던 네 가지입니다. HttpCore 4.4.5와 HttpClient 4.5.3부터 &lt;code&gt;@Contract&lt;/code&gt;로 바뀌었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;목적에_맞는_애너테이션_고르기&quot;&gt;목적에 맞는 애너테이션 고르기&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원본 JCIP 라이브러리는 라이선스 문제로, JSR-305는 표준화 중단으로 새 프로젝트에 권하기 어렵습니다.
대신 다음 절에서 볼 정적 분석 도구의 검사 범위를 고려해 아래 두 라이브러리 중 하나를 권합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;컴파일 시점 검증이 우선이라면 &lt;code&gt;error_prone_annotations&lt;/code&gt;가 적합합니다. Error Prone과 함께 쓰면 &lt;code&gt;@Immutable&lt;/code&gt;과 &lt;code&gt;@GuardedBy&lt;/code&gt;를 컴파일 시점에 검사할 수 있습니다. Guava는 이 라이브러리를 compile scope 의존성으로 선언하므로(33.7.1-jre 기준), Guava를 쓰는 프로젝트에는 전이 의존성으로 컴파일 클래스패스에 들어옵니다. 그래도 코드에서 직접 쓴다면 전이 의존성에 기대지 말고 명시적으로 의존성을 선언하는 편이 안전합니다. Error Prone은 &lt;code&gt;@ThreadSafe&lt;/code&gt;를 검사하지 않고, 이 라이브러리에는 &lt;code&gt;@NotThreadSafe&lt;/code&gt;가 없습니다. 그래서 표시가 없는 클래스를 스레드 안전하지 않다고 간주하는 관례도 필요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;공개 API에 &lt;code&gt;@NotThreadSafe&lt;/code&gt;를 포함한 JCIP 애너테이션 네 가지를 모두 표현하는 것이 우선이라면 &lt;code&gt;com.github.stephenc.jcip:jcip-annotations&lt;/code&gt;가 적합합니다. IntelliJ와 SpotBugs는 이 패키지(&lt;code&gt;net.jcip.annotations&lt;/code&gt;)를 기본 인식 목록에 넣어 두었으므로, 두 도구를 쓰는 프로젝트라면 의존성 하나만 추가하면 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정적_분석_도구의_검사_범위&quot;&gt;정적 분석 도구의 검사 범위&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;애너테이션은 문서화만으로도 가치가 있지만, 도구가 위반을 잡아 주면 더 유용합니다. SpotBugs, Error Prone, IntelliJ IDEA가 각각 어디까지 검사하는지 정리합니다. 도구의 지원 범위는 버전에 따라 바뀔 수 있으므로 이 글에서 실행하거나 문서로 확인한 버전을 먼저 밝힙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;caption class=&quot;title&quot;&gt;Table 1. 검증 기준&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 33.3333%;&quot;&gt;
&lt;col style=&quot;width: 66.6667%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;대상&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;버전과 확인 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 25&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SpotBugs&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SpotBugs 4.10.4, Gradle 플러그인 6.5.11을 예제에서 실행&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Error Prone&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Error Prone 2.50.0, Gradle 플러그인 5.1.1을 예제에서 실행&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;IntelliJ IDEA&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Inspectopedia 2026.2 문서와 &lt;a href=&quot;https://github.com/JetBrains/intellij-community/blob/826413b22cfe5b5c573662f5d2442f454dbc0b23/java/java-backend/resources/META-INF/Inspections.xml&quot;&gt;IntelliJ Community 소스 826413b22cfe&lt;/a&gt;의 인스펙션 등록 정보 기준&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SonarQube&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java 분석기 &lt;a href=&quot;https://github.com/SonarSource/sonar-java/blob/9bf31b6e003782152b920fac0b1cfa5ba55e05a1/java-checks/src/main/java/org/sonar/java/checks/VolatileNonPrimitiveFieldCheck.java&quot;&gt;SonarJava 소스 9bf31b6e0037&lt;/a&gt;의 내장 규칙과 SpotBugs 외부 규칙 목록 기준&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Eclipse JDT&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;a href=&quot;https://help.eclipse.org/latest/topic/org.eclipse.jdt.doc.isv/guide/jdt_api_options.htm&quot;&gt;JDT Core Options&lt;/a&gt;와 &lt;a href=&quot;https://github.com/eclipse-jdt/eclipse.jdt.core/tree/8c40c7d2ae12c0a32ab3cca1ab31b53956c65d51&quot;&gt;Eclipse JDT 소스 8c40c7d2ae12&lt;/a&gt; 기준&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2026년 9월 시점의 JDT Core Options 전체 목록과 Eclipse JDT 소스 8c40c7d2ae12에서는 스레드 안전성 애너테이션을 다루는 컴파일러 검사를 찾지 못했습니다. Eclipse 사용자는 &lt;a href=&quot;https://spotbugs.readthedocs.io/en/stable/eclipse.html&quot;&gt;SpotBugs Eclipse 플러그인&lt;/a&gt;으로 보완할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 시점에 SonarQube의 Java 분석기인 SonarJava 소스 9bf31b6e0037의 내장 규칙 구현을 &lt;code&gt;ThreadSafe&lt;/code&gt;, &lt;code&gt;GuardedBy&lt;/code&gt;, &lt;code&gt;javax.annotation.concurrent&lt;/code&gt;로 검색해 봤습니다. 스레드 안전성 애너테이션을 참조하는 구현은 S3077뿐이었습니다. 애너테이션으로 선언한 계약을 직접 검증하는 규칙은 찾지 못했습니다. 참조 타입 필드의 &lt;code&gt;volatile&lt;/code&gt;을 막는 &lt;a href=&quot;https://github.com/SonarSource/sonar-java/blob/9bf31b6e003782152b920fac0b1cfa5ba55e05a1/java-checks/src/main/java/org/sonar/java/checks/VolatileNonPrimitiveFieldCheck.java&quot;&gt;S3077 구현&lt;/a&gt;은 필드 타입에 JSR-305 패키지의 &lt;code&gt;@Immutable&lt;/code&gt;이나 &lt;code&gt;@ThreadSafe&lt;/code&gt;가 붙어 있으면 예외로 넘깁니다. &lt;code&gt;@GuardedBy&lt;/code&gt;는 다루지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SonarQube는 &lt;a href=&quot;https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/external-analyzer-reports&quot;&gt;SpotBugs 보고서를 외부 이슈로 가져올 수 있습니다&lt;/a&gt;. &lt;a href=&quot;https://github.com/SonarSource/sonar-java/blob/9bf31b6e003782152b920fac0b1cfa5ba55e05a1/external-reports/src/main/resources/org/sonar/l10n/java/rules/spotbugs/spotbugs-rules.json&quot;&gt;외부 규칙 목록&lt;/a&gt;에는 뒤에서 볼 세 버그 패턴이 모두 들어 있습니다. 따라서 SonarQube에서 이 결과를 보려면 SpotBugs를 빌드에서 돌려 XML 보고서를 만들고 `sonar.java.spotbugs.reportPaths`로 넘겨야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SpotBugs와 Error Prone 예제는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis&quot;&gt;examples/thread-safety-static-analysis&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;spotbugs&quot;&gt;SpotBugs&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;2079841.html&quot;&gt;FindBugs&lt;/a&gt;의 후속 프로젝트인 &lt;a href=&quot;https://spotbugs.github.io/&quot;&gt;SpotBugs&lt;/a&gt;가 JCIP 애너테이션을 위해 둔 버그 패턴은 &lt;code&gt;@Immutable&lt;/code&gt;용 하나입니다. &lt;code&gt;@GuardedBy&lt;/code&gt;와 &lt;code&gt;@ThreadSafe&lt;/code&gt;는 전용 검사가 없고, 동기화가 일관되지 않은 필드를 찾는 탐지기가 판단의 근거로 읽습니다. 인식하는 패키지는 아래 세 가지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;net.jcip.annotations&lt;/code&gt; (원본 JCIP)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;javax.annotation.concurrent&lt;/code&gt; (JSR-305)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;jakarta.annotation.concurrent&lt;/code&gt; (SpotBugs가 jakarta 이름공간 지원에 맞춰 미리 인식하는 패키지. Jakarta Annotations 3.0.0에는 이 패키지가 없음)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FindBugs 2.0 때부터 있던 &lt;a href=&quot;https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#jcip-fields-of-immutable-classes-should-be-final-jcip-field-isnt-final-in-immutable-class&quot;&gt;JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS&lt;/a&gt; 버그 패턴은 &lt;code&gt;@Immutable&lt;/code&gt;이 붙은 클래스에 &lt;code&gt;final&lt;/code&gt;이 아닌 필드가 있으면 경고합니다. 다만 SpotBugs 4.10.4의 구현은 &lt;code&gt;transient&lt;/code&gt;나 &lt;code&gt;volatile&lt;/code&gt; 필드를 이 검사에서 제외합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래처럼 &lt;code&gt;@Immutable&lt;/code&gt;로 선언했지만 setter가 있는 클래스를 만들고&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/spotbugs/src/main/java/net/benelog/Memo.java&quot;&gt;Memo.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog;

import net.jcip.annotations.Immutable;

/**
 * Declared {@code @Immutable} but has a non-final field, so
 * SpotBugs reports JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS.
 */
@Immutable
public class Memo {
	private String content;

	public void setContent(String content) {
		this.content = content;
	}

	public String getContent() {
		return content;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gradle에 &lt;a href=&quot;https://github.com/spotbugs/spotbugs-gradle-plugin&quot;&gt;SpotBugs 플러그인&lt;/a&gt;을 설정한 뒤&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/spotbugs/build.gradle.kts&quot;&gt;spotbugs/build.gradle.kts&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;kotlin&quot;&gt;plugins {
	java
	id(&quot;com.github.spotbugs&quot;) version &quot;6.5.11&quot;
}

repositories {
	mavenCentral()
}

dependencies {
	implementation(&quot;com.github.stephenc.jcip:jcip-annotations:1.0-1&quot;)
}

java {
	toolchain {
		languageVersion.set(JavaLanguageVersion.of(25))
	}
}

spotbugs {
	toolVersion.set(&quot;4.10.4&quot;)
	ignoreFailures.set(true)
	if (project.hasProperty(&quot;reportLow&quot;)) {
		// Report low-priority warnings too. The default reports medium and above only.
		reportLevel.set(com.github.spotbugs.snom.Confidence.LOW)
	}
}

tasks.spotbugsMain {
	val report = layout.buildDirectory.file(&quot;reports/spotbugs/main.txt&quot;)
	reports.create(&quot;text&quot;) {
		required.set(true)
		outputLocation.set(report)
	}
	doLast {
		println(report.get().asFile.readText())
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew :spotbugs:spotbugsMain&lt;/code&gt;을 실행하면 보고서에 다음 줄이 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;M B JCIP: Memo.content should be final since net.benelog.Memo is marked as Immutable.  In Memo.java&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예제는 경고가 있어도 보고서를 끝까지 출력하려고 &lt;code&gt;ignoreFailures&lt;/code&gt;를 &lt;code&gt;true&lt;/code&gt;로 두었습니다. 실제 CI에서 위반 때문에 빌드를 실패시키려면 이 설정을 &lt;code&gt;false&lt;/code&gt;로 바꾸거나 생략해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@GuardedBy&lt;/code&gt;는 &lt;a href=&quot;https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#is-field-not-guarded-against-concurrent-access-is-field-not-guarded&quot;&gt;IS_FIELD_NOT_GUARDED&lt;/a&gt; 버그 패턴이 다룹니다. 그런데 이 패턴은 애너테이션을 보고 위반을 찾는 것이 아닙니다. &lt;a href=&quot;https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html#is-inconsistent-synchronization-is2-inconsistent-sync&quot;&gt;IS2_INCONSISTENT_SYNC&lt;/a&gt; 탐지기는 필드 접근 중 lock을 잡은 비율로 동기화 누락을 추정합니다. 필드에 &lt;code&gt;@GuardedBy(&quot;this&quot;)&lt;/code&gt;가 붙어 있으면 이 탐지기는 세 가지를 다르게 처리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;버그 패턴 이름을 IS_FIELD_NOT_GUARDED로 바꿉니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;lock을 잡은 접근이 없는 필드도 후보에 남깁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;경고 우선순위를 올립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인식하는 값도 &lt;code&gt;&quot;this&quot;&lt;/code&gt;뿐입니다. 이 탐지기는 lock을 잡은 접근이 절반에 못 미치는 필드는 오탐으로 간주해 우선순위를 낮춥니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 같은 프로젝트의 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/spotbugs/src/main/java/net/benelog/Counter.java&quot;&gt;Counter&lt;/a&gt;는 &lt;code&gt;@GuardedBy(&quot;this&quot;)&lt;/code&gt; 필드를 lock 없이 수정하는데도 기본 설정에서는 보고되지 않습니다. &lt;code&gt;increment()&lt;/code&gt;의 읽기와 쓰기는 lock 없이 이루어지고, &lt;code&gt;synchronized&lt;/code&gt; 메서드 &lt;code&gt;get()&lt;/code&gt;의 읽기만 lock을 잡습니다. 세 번의 접근 중 lock을 잡은 접근은 33%입니다. 예제 저장소에서 &lt;code&gt;./gradlew :spotbugs:spotbugsMain -PreportLow&lt;/code&gt;로 낮은 우선순위까지 보고하게 하면 그제야 나타납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;L M IS: Counter.count not guarded against concurrent access; locked 33% of time  Unsynchronized access at Counter.java:[line 16]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 위반에 &lt;code&gt;synchronized&lt;/code&gt; 메서드 두 개를 더한 아래 클래스는 lock을 잡은 접근이 60%가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/spotbugs/src/main/java/net/benelog/LockedCounter.java&quot;&gt;LockedCounter.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

/**
 * Same violation as Counter, but more synchronized methods raise
 * the proportion of locked accesses. SpotBugs reports IS_FIELD_NOT_GUARDED.
 */
@ThreadSafe
public class LockedCounter {
	@GuardedBy(&quot;this&quot;)
	private int count;

	public void increment() {
		count++;
	}

	public synchronized int get() {
		return count;
	}

	public synchronized void reset() {
		count = 0;
	}

	public synchronized boolean isZero() {
		return count == 0;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 클래스는 기본 설정에서도 높은 우선순위로 보고됩니다. 출력의 첫 글자는 우선순위, 둘째 글자는 분류입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;H M IS: LockedCounter.count not guarded against concurrent access; locked 60% of time  Unsynchronized access at LockedCounter.java:[line 16]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@ThreadSafe&lt;/code&gt;와 &lt;code&gt;@NotThreadSafe&lt;/code&gt;도 이 탐지기가 읽습니다. &lt;code&gt;@NotThreadSafe&lt;/code&gt; 클래스의 필드는 검사에서 제외하고, &lt;code&gt;@ThreadSafe&lt;/code&gt; 클래스의 필드는 경고 우선순위를 올리는 근거로 씁니다. 애너테이션으로 선언한 계약이 지켜졌는지 검증하지는 않습니다. &lt;code&gt;@ThreadSafe&lt;/code&gt;로 선언한 클래스가 &lt;code&gt;@NotThreadSafe&lt;/code&gt; 타입의 필드를 두어도 경고하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하면 SpotBugs가 애너테이션만 보고 결정적으로 검사하는 것은 &lt;code&gt;@Immutable&lt;/code&gt; 클래스의 &lt;code&gt;final&lt;/code&gt; 규칙뿐입니다. &lt;code&gt;@GuardedBy&lt;/code&gt; 위반도 결국 lock을 잡은 접근 비율에 기댄 추정으로 보고되며, 애너테이션은 그 추정의 후보와 우선순위를 바꿀 뿐입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;error_prone&quot;&gt;Error Prone&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Google의 &lt;a href=&quot;https://errorprone.info/&quot;&gt;Error Prone&lt;/a&gt;은 &lt;code&gt;javac&lt;/code&gt;에 플러그인으로 붙어서 컴파일 시점에 에러를 예방하는 규칙을 검사합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;guardedby_검사&quot;&gt;@GuardedBy 검사&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Error Prone의 &lt;a href=&quot;https://errorprone.info/bugpattern/GuardedBy&quot;&gt;GuardedBy 검사&lt;/a&gt;는 &lt;code&gt;@GuardedBy(lock)&lt;/code&gt;이 붙은 필드나 메서드를 지정한 lock을 잡지 않고 접근하면 컴파일 오류를 냅니다. 접근 비율에 따라 보고 여부가 달라지는 SpotBugs와 달리, 분석 범위 안의 접근마다 lock을 잡았는지 판정합니다. 다만 생성자와 초기화 블록 안의 접근, `ReadWriteLock`으로 보호하는 필드는 검사하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 검사가 인식하는 애너테이션에는 다음이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;com.google.errorprone.annotations.concurrent.GuardedBy&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;javax.annotation.concurrent.GuardedBy&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Android용 &lt;code&gt;androidx.annotation.GuardedBy&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원본 JCIP의 &lt;code&gt;net.jcip.annotations.GuardedBy&lt;/code&gt;는 목록에 없습니다. &lt;code&gt;@Immutable&lt;/code&gt; 검사는 &lt;code&gt;com.google.errorprone.annotations.Immutable&lt;/code&gt;만 검사 대상으로 하고 &lt;code&gt;javax.annotation.concurrent.Immutable&lt;/code&gt;은 제외한다고 문서에 명시되어 있습니다. 다음 절에서 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Gradle에서는 &lt;a href=&quot;https://github.com/tbroyer/gradle-errorprone-plugin&quot;&gt;net.ltgt.errorprone 플러그인&lt;/a&gt;으로 Error Prone을 &lt;code&gt;javac&lt;/code&gt;에 붙입니다. 세 가지 패키지의 애너테이션을 비교하기 위해 라이브러리도 셋 다 넣었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/build.gradle.kts&quot;&gt;errorprone/build.gradle.kts&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;kotlin&quot;&gt;plugins {
	java
	id(&quot;net.ltgt.errorprone&quot;) version &quot;5.1.1&quot;
}

repositories {
	mavenCentral()
}

dependencies {
	implementation(&quot;com.github.stephenc.jcip:jcip-annotations:1.0-1&quot;)
	implementation(&quot;com.google.code.findbugs:jsr305:3.0.2&quot;)
	implementation(&quot;com.google.errorprone:error_prone_annotations:2.50.0&quot;)
	errorprone(&quot;com.google.errorprone:error_prone_core:2.50.0&quot;)
}

java {
	toolchain {
		languageVersion.set(JavaLanguageVersion.of(25))
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실제로 JSR-305의 &lt;code&gt;@GuardedBy&lt;/code&gt;를 쓴 아래 클래스를&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/src/main/java/net/benelog/JsrCounter.java&quot;&gt;JsrCounter.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

/**
 * JSR-305 package. Error Prone reports the @GuardedBy violation as a compile error.
 */
@ThreadSafe
public class JsrCounter {
	@GuardedBy(&quot;this&quot;)
	private int count;

	public void increment() {
		count++;
	}

	public synchronized int get() {
		return count;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컴파일하면 Error Prone 2.50.0이 다음 오류를 냅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;JsrCounter.java:15: error: [GuardedBy] This access should be guarded by &apos;this&apos;, which is not currently held
		count++;
		^
    (see https://errorprone.info/bugpattern/GuardedBy)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 코드에서 import만 &lt;code&gt;net.jcip.annotations.GuardedBy&lt;/code&gt;로 바꾼 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/src/main/java/net/benelog/JcipCounter.java&quot;&gt;JcipCounter&lt;/a&gt;는 아무 오류 없이 컴파일됩니다. 반대로 Error Prone 자체 패키지의 &lt;code&gt;@GuardedBy&lt;/code&gt;를 쓴 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/src/main/java/net/benelog/ErrorProneCounter.java&quot;&gt;ErrorProneCounter&lt;/a&gt;는 JsrCounter와 같은 오류로 잡힙니다. 세 클래스는 예제 저장소의 errorprone 서브프로젝트에 있고 &lt;code&gt;./gradlew :errorprone:compileJava&lt;/code&gt;로 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;immutable_검사&quot;&gt;@Immutable 검사&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://errorprone.info/bugpattern/Immutable&quot;&gt;Immutable 검사&lt;/a&gt;는 &lt;code&gt;com.google.errorprone.annotations.Immutable&lt;/code&gt;이 붙은 클래스가 깊은 불변인지 검사합니다. SpotBugs의 JCIP 검사는 필드가 &lt;code&gt;final&lt;/code&gt;인지만 보지만, Error Prone은 참조 필드의 타입이 불변인지도 봅니다. Error Prone 문서가 설명하는 보수적인 불변성의 정의에는 생성자에서 &lt;code&gt;this&lt;/code&gt;가 밖으로 새지 않아야 한다는 조건도 있습니다. 그러나 2.50.0의 &lt;code&gt;ImmutableChecker&lt;/code&gt;는 일반적인 생성자 본문의 &lt;code&gt;this&lt;/code&gt; 탈출까지 분석하지는 않습니다. 아래 클래스에는 &lt;code&gt;final&lt;/code&gt;이 아닌 필드와 &lt;code&gt;final&lt;/code&gt;이지만 타입이 가변인 필드가 하나씩 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/src/main/java/net/benelog/ErrorProneMemo.java&quot;&gt;ErrorProneMemo.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog;

import java.util.List;

import com.google.errorprone.annotations.Immutable;

/**
 * Error Prone&apos;s own @Immutable. Both the non-final field and
 * the final field of a mutable type are reported as compile errors.
 */
@Immutable
public class ErrorProneMemo {
	private String content;
	private final List&amp;lt;String&amp;gt; tags;

	public ErrorProneMemo(String content, List&amp;lt;String&amp;gt; tags) {
		this.content = content;
		this.tags = tags;
	}

	public String getContent() {
		return content;
	}

	public List&amp;lt;String&amp;gt; getTags() {
		return tags;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 필드가 모두 컴파일 오류로 보고됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;ErrorProneMemo.java:13: error: [Immutable] type annotated with @Immutable could not be proven immutable: &apos;ErrorProneMemo&apos; has non-final field &apos;content&apos;
	private String content;
	               ^
    (see https://errorprone.info/bugpattern/Immutable)
  Did you mean &apos;private final String content;&apos;?
ErrorProneMemo.java:14: error: [Immutable] type annotated with @Immutable could not be proven immutable: &apos;ErrorProneMemo&apos; has field &apos;tags&apos; of type &apos;java.util.List&amp;lt;java.lang.String&amp;gt;&apos;, &apos;List&apos; is mutable
	private final List&amp;lt;String&amp;gt; tags;
	                           ^
    (see https://errorprone.info/bugpattern/Immutable)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;tags&lt;/code&gt;가 통과하려면 Error Prone이 불변으로 아는 타입이거나 &lt;code&gt;@Immutable&lt;/code&gt;이 붙은 타입이어야 합니다. &lt;code&gt;@Immutable&lt;/code&gt;이 붙은 인터페이스를 구현한 클래스도 같은 검사를 받습니다. 제네릭 클래스는 &lt;code&gt;containerOf&lt;/code&gt; 속성으로 어떤 타입 파라미터가 불변이어야 하는지 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 코드에서 import만 &lt;code&gt;javax.annotation.concurrent.Immutable&lt;/code&gt;로 바꾼 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/src/main/java/net/benelog/JsrMemo.java&quot;&gt;JsrMemo&lt;/a&gt;는 오류 없이 컴파일됩니다. &lt;code&gt;@GuardedBy&lt;/code&gt;는 JSR-305 패키지도 검사하지만 &lt;code&gt;@Immutable&lt;/code&gt;은 Error Prone 자체 패키지만 검사합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;threadsafe_검사&quot;&gt;@ThreadSafe 검사&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;com.google.errorprone.annotations.ThreadSafe&lt;/code&gt;에도 &lt;a href=&quot;https://errorprone.info/bugpattern/ThreadSafe&quot;&gt;ThreadSafe 검사&lt;/a&gt; 문서가 있습니다. 버그 패턴 목록에서 &apos;Experimental&apos; 그룹에 있어서 켜기만 하면 될 것처럼 보이지만, 실제로는 켤 수 없습니다. Gradle 플러그인에서 &lt;code&gt;options.errorprone.error(&quot;ThreadSafe&quot;)&lt;/code&gt;로 켜면 &apos;ThreadSafe is not a valid checker name&apos; 오류로 빌드가 실패합니다. Error Prone 2.50.0 소스의 내장 검사 목록인 &lt;code&gt;BuiltInCheckerSuppliers&lt;/code&gt;에 &lt;code&gt;GuardedByChecker&lt;/code&gt;와 &lt;code&gt;ImmutableChecker&lt;/code&gt;는 있지만 &lt;code&gt;ThreadSafeChecker&lt;/code&gt;는 없습니다. 확인해 본 2.3.0부터 2.45.0까지의 이전 버전들에도 없었습니다. 검사기 클래스 자체는 &lt;code&gt;error_prone_core&lt;/code&gt; jar에 들어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 아래 클래스는 기본 설정에서 아무 오류 없이 컴파일됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/src/main/java/net/benelog/ErrorProneRegistry.java&quot;&gt;ErrorProneRegistry.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.google.errorprone.annotations.ThreadSafe;

/**
 * Error Prone&apos;s own @ThreadSafe. Error Prone 2.50.0 does not check it by default.
 * Registering ThreadSafeChecker with -PthreadSafeCheck reports fields modified without a lock
 * and final fields of non-thread-safe types as compile errors.
 */
@ThreadSafe
public class ErrorProneRegistry {
	private int count;
	private final Map&amp;lt;String, String&amp;gt; entries = new HashMap&amp;lt;&amp;gt;();
	private final ConcurrentHashMap&amp;lt;String, String&amp;gt; safeEntries = new ConcurrentHashMap&amp;lt;&amp;gt;();

	public void register(String key, String value) {
		count++;
		entries.put(key, value);
		safeEntries.put(key, value);
	}

	public int getCount() {
		return count;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 검사기가 무엇을 보는지 확인하려고 예제 저장소에 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/threadsafe-check&quot;&gt;threadsafe-check&lt;/a&gt; 서브프로젝트를 만들었습니다. &lt;code&gt;ThreadSafeChecker&lt;/code&gt;를 상속한 클래스를 &lt;code&gt;META-INF/services&lt;/code&gt;에 등록해서 Error Prone의 플러그인 검사로 불러오는 실험용 코드입니다. Error Prone은 컴파일러의 애너테이션 프로세서 경로(processor path)에서 &lt;code&gt;com.google.errorprone.bugpatterns.BugChecker&lt;/code&gt;를 서비스 인터페이스로 삼아 ServiceLoader로 검사기를 찾습니다. 그래서 그 이름의 파일에 검사기 클래스 이름을 적어 두면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/threadsafe-check/src/main/resources/META-INF/services/com.google.errorprone.bugpatterns.BugChecker&quot;&gt;META-INF/services/com.google.errorprone.bugpatterns.BugChecker&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;com.google.errorprone.bugpatterns.threadsafety.ThreadSafeCheck&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;등록한 클래스는 &lt;code&gt;ThreadSafeChecker&lt;/code&gt;를 상속하고 &lt;code&gt;@BugPattern&lt;/code&gt;으로 검사 이름을 붙인 래퍼입니다. &lt;code&gt;ThreadSafeChecker&lt;/code&gt;의 생성자가 패키지 전용이라 같은 패키지 이름을 썼습니다. ServiceLoader가 요구하는 public 기본 생성자도 형식적으로 두었습니다. 이렇게 2.50.0의 내부 구조에 기댄 코드라서 실제 프로젝트에는 권하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/threadsafe-check/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ThreadSafeCheck.java&quot;&gt;ThreadSafeCheck.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package com.google.errorprone.bugpatterns.threadsafety;

@BugPattern(
		name = &quot;ThreadSafe&quot;,
		summary = &quot;Type declaration annotated with @ThreadSafe is not thread safe&quot;,
		severity = ERROR)
public class ThreadSafeCheck extends ThreadSafeChecker {

	/** Public no-argument constructor required by ServiceLoader. Error Prone uses the @Inject constructor. */
	public ThreadSafeCheck() {
		super(null, null);
	}

	@Inject
	ThreadSafeCheck(WellKnownThreadSafety wellKnownThreadSafety,
			ThreadSafeAnalysis.Factory threadSafeAnalysisFactory) {
		super(wellKnownThreadSafety, threadSafeAnalysisFactory);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 서브프로젝트를 검사 대상 프로젝트의 &lt;code&gt;errorprone&lt;/code&gt; 구성에 의존성으로 넣으면 플러그인 검사로 등록됩니다. 예제에서는 Gradle 프로퍼티가 있을 때만 넣도록 했고, 앞에서 인용한 &lt;code&gt;build.gradle.kts&lt;/code&gt;에서는 이 부분을 생략했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-static-analysis/errorprone/build.gradle.kts&quot;&gt;errorprone/build.gradle.kts&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;kotlin&quot;&gt;dependencies {
	errorprone(&quot;com.google.errorprone:error_prone_core:2.50.0&quot;)
	if (project.hasProperty(&quot;threadSafeCheck&quot;)) {
		// Register ThreadSafeChecker, which is not in the default check list, as a plugin.
		errorprone(project(&quot;:threadsafe-check&quot;))
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;./gradlew :errorprone:compileJava -PthreadSafeCheck&lt;/code&gt;로 실행하면 다음과 같이 보고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;ErrorProneRegistry.java:16: error: [ThreadSafe] @ThreadSafe class fields should be final or annotated with @GuardedBy. See https://errorprone.info/bugpattern/ThreadSafe for details.
	private int count;
	            ^
    (see https://errorprone.info/bugpattern/ThreadSafe)
  Did you mean &apos;private final int count;&apos;?
ErrorProneRegistry.java:17: error: [ThreadSafe] @ThreadSafe class has non-thread-safe field, &apos;Map&apos; is not thread-safe
	private final Map&amp;lt;String, String&amp;gt; entries = new HashMap&amp;lt;&amp;gt;();
	                                  ^
    (see https://errorprone.info/bugpattern/ThreadSafe)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 검사를 통과하려면 인스턴스 필드가 다음 둘 중 하나여야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;final&lt;/code&gt;이면서 선언 타입이 Error Prone이 스레드 안전하다고 아는 타입&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@GuardedBy&lt;/code&gt;가 붙은 필드&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;static&lt;/code&gt; 필드는 검사하지 않고, &lt;code&gt;@LazyInit&lt;/code&gt; 필드는 &lt;code&gt;final&lt;/code&gt; 조건에서 제외합니다. &lt;code&gt;count&lt;/code&gt;는 &lt;code&gt;final&lt;/code&gt;도 &lt;code&gt;@GuardedBy&lt;/code&gt;도 없어서 걸립니다. &lt;code&gt;entries&lt;/code&gt;는 선언 타입 &lt;code&gt;Map&lt;/code&gt;이 스레드 안전 타입이 아니라서 걸립니다. 실제 객체가 &lt;code&gt;ConcurrentHashMap&lt;/code&gt;이어도 선언 타입이 &lt;code&gt;Map&lt;/code&gt;이면 걸립니다. 선언 타입까지 &lt;code&gt;ConcurrentHashMap&lt;/code&gt;인 &lt;code&gt;safeEntries&lt;/code&gt;는 통과합니다. 앞에서 본 ErrorProneCounter는 &lt;code&gt;count&lt;/code&gt;에 &lt;code&gt;@GuardedBy(&quot;this&quot;)&lt;/code&gt;가 있어서 이 검사는 통과하고 &lt;code&gt;@GuardedBy&lt;/code&gt; 검사에만 걸립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하면 2026년 9월 시점의 Error Prone은 &lt;code&gt;@Immutable&lt;/code&gt;과 &lt;code&gt;@GuardedBy&lt;/code&gt;는 검사하지만, &lt;code&gt;@ThreadSafe&lt;/code&gt;는 위처럼 검사기를 직접 플러그인으로 등록하지 않는 한 문서 역할만 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;intellij_idea&quot;&gt;IntelliJ IDEA&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IntelliJ IDEA 2026.2는 &lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/Java-Concurrency-annotation-issues.html&quot;&gt;Concurrency annotation issues&lt;/a&gt; 그룹에 인스펙션이 여섯 개 있습니다. 다섯 개는 &lt;code&gt;@GuardedBy&lt;/code&gt;용이고 하나는 &lt;code&gt;@Immutable&lt;/code&gt;용입니다. &lt;code&gt;@ThreadSafe&lt;/code&gt;로 선언한 계약을 검증하는 인스펙션은 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;img/thread-safety-annotations/intellij-concurrency-inspections.png&quot; alt=&quot;IntelliJ IDEA 2026.2의 Concurrency annotation issues 인스펙션 목록&quot; width=&quot;800&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/FieldAccessNotGuarded.html&quot;&gt;Unguarded field access or method call&lt;/a&gt; 인스펙션은 다음 패키지의 &lt;code&gt;@GuardedBy&lt;/code&gt;를 모두 인식합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;net.jcip.annotations&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;javax.annotation.concurrent&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;org.apache.http.annotation&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;com.android.annotations.concurrency&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;androidx.annotation&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;com.google.errorprone.annotations.concurrent&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SpotBugs도 원본 JCIP 패키지를 읽지만 &lt;code&gt;@GuardedBy(&quot;this&quot;)&lt;/code&gt;만 다루고 접근 비율로 짐작해 경고하는 데 그칩니다. IntelliJ는 이에 비해 원본 JCIP의 guard 표현을 직접 검사합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/NonFinalFieldInImmutable.html&quot;&gt;Non-final field in @Immutable class&lt;/a&gt; 인스펙션은 &lt;code&gt;@Immutable&lt;/code&gt; 클래스에 &lt;code&gt;final&lt;/code&gt;이 아닌 필드가 있으면 경고합니다. 필드 타입이 가변인지는 보지 않으므로 검사 깊이는 Error Prone이 아니라 SpotBugs에 가깝습니다. 다만 SpotBugs와 달리 &lt;code&gt;transient`나 `volatile&lt;/code&gt; 필드도 경고합니다. 이 인스펙션이 인식하는 표시는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;원본 JCIP와 JSR-305 패키지의 &lt;code&gt;@Immutable&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Error Prone의 &lt;code&gt;com.google.errorprone.annotations.Immutable&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AutoValue의 &lt;code&gt;@AutoValue&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Javadoc의 &lt;code&gt;@Immutable&lt;/code&gt; 태그&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@ThreadSafe&lt;/code&gt;는 &apos;Threading issues&apos; 그룹의 &lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/AccessToStaticFieldLockedOnInstance.html&quot;&gt;Access to static field locked on instance&lt;/a&gt; 인스펙션이 힌트로만 읽습니다. 이 인스펙션은 인스턴스 lock을 잡고 상수가 아닌 &lt;code&gt;static&lt;/code&gt; 필드에 접근하면 경고합니다. 접근한 필드가 &lt;code&gt;final&lt;/code&gt;이면 &lt;a href=&quot;https://github.com/JetBrains/intellij-community/blob/826413b22cfe5b5c573662f5d2442f454dbc0b23/java/java-impl-inspections/src/com/siyeh/ig/threading/AccessToStaticFieldLockedOnInstanceInspection.java&quot;&gt;그 선언 타입의 애너테이션을 확인&lt;/a&gt;하고, 등록된 스레드 안전 애너테이션이 있으면 경고를 생략합니다. 따라서 애너테이션이 붙은 클래스 자체의 계약을 검증하지는 않습니다. &lt;a href=&quot;https://github.com/JetBrains/intellij-community/blob/826413b22cfe5b5c573662f5d2442f454dbc0b23/java/java-psi-api/src/com/intellij/codeInsight/ConcurrencyAnnotationsManager.java&quot;&gt;기본 목록&lt;/a&gt;에는 다음 애너테이션이 들어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;원본 JCIP, JSR-305, &lt;code&gt;org.apache.http.annotation&lt;/code&gt;, &lt;code&gt;com.android.annotations.concurrency&lt;/code&gt; 패키지의 &lt;code&gt;@ThreadSafe&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;androidx.annotation&lt;/code&gt;, &lt;code&gt;android.support.annotation&lt;/code&gt; 패키지의 &lt;code&gt;@AnyThread&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Error Prone의 &lt;code&gt;@ThreadSafe&lt;/code&gt;는 기본 목록에 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 IntelliJ IDEA 2026.2의 &lt;a href=&quot;https://github.com/JetBrains/intellij-community/blob/826413b22cfe5b5c573662f5d2442f454dbc0b23/java/java-backend/resources/META-INF/Inspections.xml&quot;&gt;인스펙션 등록 정보&lt;/a&gt;를 보면 &apos;Concurrency annotation issues&apos; 그룹은 모두 &lt;code&gt;enabledByDefault=&quot;false&quot;&lt;/code&gt;, &lt;code&gt;level=&quot;WARNING&quot;&lt;/code&gt;으로 선언되어 있습니다. Settings의 Editor | Inspections | Java | Concurrency annotation issues에서 직접 켜야 합니다. 켜도 편집기 안의 경고라서 Error Prone처럼 &lt;code&gt;javac&lt;/code&gt; 컴파일을 막지는 못합니다. CI에서 강제하려면 Qodana 같은 별도 실행 환경이 필요합니다. &lt;a href=&quot;https://www.jetbrains.com/help/qodana/about-qodana.html&quot;&gt;Qodana&lt;/a&gt;는 JetBrains가 만든 코드 품질 분석 플랫폼으로, IntelliJ IDEA와 같은 인스펙션을 Docker 이미지나 명령행 도구로 CI에서 실행합니다. 따라서 &lt;a href=&quot;https://www.jetbrains.com/help/qodana/inspection-profiles.html&quot;&gt;Qodana 프로필&lt;/a&gt;에서 검사를 켜고 &lt;a href=&quot;https://www.jetbrains.com/help/qodana/quality-gate.html&quot;&gt;실패 조건&lt;/a&gt;을 설정하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세 도구를 정리하면 애너테이션만 보고 결정적으로 검사하는 범위는 좁습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SpotBugs는 &lt;code&gt;@Immutable&lt;/code&gt; 클래스의 &lt;code&gt;final&lt;/code&gt; 규칙만 직접 검사합니다. &lt;code&gt;@GuardedBy&lt;/code&gt;는 lock을 잡은 접근 비율로 짐작하며, &lt;code&gt;@ThreadSafe&lt;/code&gt;와 &lt;code&gt;@NotThreadSafe&lt;/code&gt;는 그 짐작의 근거로만 읽습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Error Prone은 &lt;code&gt;@GuardedBy&lt;/code&gt;의 lock 누락과 &lt;code&gt;@Immutable&lt;/code&gt;의 깊은 불변성을 컴파일 오류로 잡습니다. 다만 원본 JCIP 패키지는 인식하지 않고, &lt;code&gt;@ThreadSafe&lt;/code&gt; 검사는 따로 등록해야 켜집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ IDEA는 원본 JCIP를 포함한 여러 패키지의 &lt;code&gt;@GuardedBy&lt;/code&gt;를 직접 검사하고, &lt;code&gt;@Immutable&lt;/code&gt;은 SpotBugs와 비슷한 깊이로 봅니다. 그러나 인스펙션이 기본으로 꺼져 있고 &lt;code&gt;javac&lt;/code&gt; 컴파일에는 연동되지 않습니다. 강제하려면 Qodana나 명령행 검사 같은 별도 실행 환경이 필요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 &lt;code&gt;error_prone_annotations&lt;/code&gt;와 &lt;code&gt;jcip-annotations&lt;/code&gt; 중 하나를 권한 이유가 여기에 있습니다. 컴파일을 막는 검사가 필요하면 Error Prone이 인식하는 애너테이션을 써야 합니다. 네 가지 애너테이션을 모두 표기하면서 IDE와 SpotBugs의 검사를 받으려면 &lt;code&gt;net.jcip.annotations&lt;/code&gt; 패키지(Apache 라이선스 재구현)를 써야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;archunit으로_검증하는_프로젝트별_규칙&quot;&gt;ArchUnit으로 검증하는 프로젝트별 규칙&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞의 정적 분석 도구들은 애너테이션이 붙은 클래스 자체가 선언대로 구현되었는지를 일부 검사합니다. 그런데 애플리케이션 코드에서는 다른 형태의 사고도 생깁니다. 여러 스레드가 공유하는 객체가, 스레드 안전하지 않다고 표시된 클래스를 동기화 없이 필드로 두는 경우입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들어 Spring의 &lt;code&gt;@RestController&lt;/code&gt;나 &lt;code&gt;@Service&lt;/code&gt; 빈은 별도의 scope를 지정하지 않으면 singleton이라서 여러 요청 스레드가 필드를 공유합니다. request scope 같은 다른 scope를 쓸 수도 있습니다. 그러나 프로젝트의 정책으로 컨트롤러가 이런 타입을 필드로 보유하는 것 자체를 금지해서 이 문제를 예방하기도 합니다.
이런 규칙은 프로젝트에서 추구하는 구조에 따라 달라지므로 범용 정적 분석 도구에는 없습니다. &lt;a href=&quot;https://www.archunit.org/&quot;&gt;ArchUnit&lt;/a&gt;을 쓰면 이 규칙을 JUnit 테스트로 작성해서 빌드마다 검사할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;예제_코드&quot;&gt;예제 코드&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예제로 &lt;code&gt;@NotThreadSafe&lt;/code&gt;가 붙은 클래스와, 그 클래스를 필드로 가진 컨트롤러를 만들었습니다. 이 컨트롤러에는 &lt;code&gt;SimpleDateFormat&lt;/code&gt; 필드도 있습니다. 전체 프로젝트는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-archunit&quot;&gt;examples/thread-safety-archunit&lt;/a&gt;에 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-archunit/src/main/java/net/benelog/report/ReportFormatter.java&quot;&gt;ReportFormatter.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog.report;

import net.jcip.annotations.NotThreadSafe;

@NotThreadSafe
public class ReportFormatter {
	private final StringBuilder buffer = new StringBuilder();

	public String format(String title, String body) {
		buffer.setLength(0);
		return buffer.append(title).append(&apos;\n&apos;).append(body).toString();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-archunit/src/main/java/net/benelog/web/ReportController.java&quot;&gt;ReportController.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog.web;

import java.text.SimpleDateFormat;
import java.util.Date;

import net.benelog.report.ReportFormatter;
import net.benelog.report.ReportService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ReportController {
	private final ReportService reportService;
	private final ReportFormatter formatter = new ReportFormatter();
	private final SimpleDateFormat dateFormat = new SimpleDateFormat(&quot;yyyy-MM-dd&quot;);

	public ReportController(ReportService reportService) {
		this.reportService = reportService;
	}

	@GetMapping(&quot;/reports/{id}&quot;)
	public String report(@PathVariable long id) {
		return formatter.format(dateFormat.format(new Date()), reportService.find(id));
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;규칙과_실행_결과&quot;&gt;규칙과 실행 결과&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ArchUnit 테스트는 규칙 두 개로 구성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫 번째 규칙은 &lt;code&gt;@RestController&lt;/code&gt;가 붙은 클래스의 필드 타입에 &lt;code&gt;@NotThreadSafe&lt;/code&gt;가 붙어 있으면 외부 동기화 여부와 빈 scope를 따로 분석하지 않고 실패합니다. ArchUnit은 바이트코드에서 애너테이션을 읽습니다. 그래서 원본 JCIP 애너테이션 같은 RUNTIME retention이든, JSR-305 애너테이션이나 HttpClient &lt;code&gt;@Contract&lt;/code&gt; 같은 CLASS retention이든 모두 검사할 수 있습니다. 다만 예제 규칙은 &lt;code&gt;net.jcip.annotations.NotThreadSafe&lt;/code&gt;만 보므로, 다른 패키지의 애너테이션이나 &lt;code&gt;@Contract&lt;/code&gt;의 &lt;code&gt;threading&lt;/code&gt; 값을 검사하려면 조건을 추가해야 합니다. 분석 대상 패키지 밖에 있는 라이브러리 클래스도 기본 설정에서는 클래스패스에서 읽어 오므로, 라이브러리가 붙인 &lt;code&gt;@NotThreadSafe&lt;/code&gt;도 같은 규칙으로 검사할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 번째 규칙은 &lt;code&gt;SimpleDateFormat&lt;/code&gt;처럼 스레드 안전성 애너테이션이 없는 JDK 클래스를 위한 것으로, &lt;code&gt;Format&lt;/code&gt;, &lt;code&gt;Calendar&lt;/code&gt;, &lt;code&gt;StringBuilder&lt;/code&gt;를 목록으로 지정했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-archunit/src/test/java/net/benelog/ThreadSafetyArchTest.java&quot;&gt;ThreadSafetyArchTest.java&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package net.benelog;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;

import java.text.Format;
import java.util.Calendar;

import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import net.jcip.annotations.NotThreadSafe;
import org.springframework.web.bind.annotation.RestController;

@AnalyzeClasses(packages = &quot;net.benelog&quot;)
class ThreadSafetyArchTest {

	@ArchTest
	static final ArchRule controllers_should_not_hold_not_thread_safe_types =
			fields().that().areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
					.should().notHaveRawType(annotatedWith(NotThreadSafe.class))
					.because(&quot;controllers are singletons by default, so all request threads share their fields&quot;);

	private static final DescribedPredicate&amp;lt;JavaClass&amp;gt; KNOWN_NOT_THREAD_SAFE_JDK_TYPES =
			assignableTo(Format.class)
					.or(assignableTo(Calendar.class))
					.or(assignableTo(StringBuilder.class))
					.as(&quot;JDK types that are not thread-safe (Format, Calendar, StringBuilder)&quot;);

	@ArchTest
	static final ArchRule controllers_should_not_hold_known_not_thread_safe_jdk_types =
			fields().that().areDeclaredInClassesThat().areAnnotatedWith(RestController.class)
					.should().notHaveRawType(KNOWN_NOT_THREAD_SAFE_JDK_TYPES)
					.because(&quot;JDK classes carry no thread safety annotations, so a list blocks them&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ArchUnit 1.5.0, Spring Web 7.0.9, JUnit 6.1.3, JDK 25에서 &lt;code&gt;./gradlew test&lt;/code&gt;를 실행하면 두 규칙 모두 실패하고, 어떤 필드가 규칙을 어겼는지 알려 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Architecture Violation [Priority: MEDIUM] - Rule &apos;fields that are declared in classes that are annotated with @RestController should not have raw type annotated with @NotThreadSafe, because controllers are singletons by default, so all request threads share their fields&apos; was violated (1 times):
Field &amp;lt;net.benelog.web.ReportController.formatter&amp;gt; has raw type annotated with @NotThreadSafe in (ReportController.java:0)

Architecture Violation [Priority: MEDIUM] - Rule &apos;fields that are declared in classes that are annotated with @RestController should not have raw type JDK types that are not thread-safe (Format, Calendar, StringBuilder), because JDK classes carry no thread safety annotations, so a list blocks them&apos; was violated (1 times):
Field &amp;lt;net.benelog.web.ReportController.dateFormat&amp;gt; has raw type JDK types that are not thread-safe (Format, Calendar, StringBuilder) in (ReportController.java:0)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 프로젝트에서 &lt;code&gt;Clock&lt;/code&gt;과 &lt;code&gt;DateTimeFormatter&lt;/code&gt;만 필드로 가진 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/thread-safety-archunit/src/main/java/net/benelog/web/HealthController.java&quot;&gt;HealthController&lt;/a&gt;는 두 규칙을 통과했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;규칙에 걸린 &lt;code&gt;ReportController&lt;/code&gt;는 &lt;code&gt;SimpleDateFormat&lt;/code&gt; 대신 불변인 &lt;code&gt;DateTimeFormatter&lt;/code&gt;를 필드로 두고, &lt;code&gt;ReportFormatter&lt;/code&gt;를 요청을 처리하는 메서드의 지역 변수로 가두는 식으로 고칠 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd&quot;);

@GetMapping(&quot;/reports/{id}&quot;)
public String report(@PathVariable long id) {
	ReportFormatter formatter = new ReportFormatter();
	return formatter.format(DATE_FORMAT.format(LocalDate.now()), reportService.find(id));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;ReportController&lt;/code&gt;를 이렇게 바꾸고 테스트를 다시 실행하면 두 규칙을 모두 통과합니다. &lt;code&gt;ReportFormatter&lt;/code&gt;는 호출마다 새로 만들어져 다른 요청 스레드와 공유되지 않습니다. 그러나 규칙을 통과했다고 해서 컨트롤러 전체의 스레드 안전성이 증명되지는 않습니다. 다른 공유 상태나 메서드 호출 순서의 경쟁 조건은 별도로 검토해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 방식에는 한계도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ArchUnit은 이 규칙에서 필드의 raw 선언 타입만 봅니다. &lt;code&gt;List&lt;/code&gt;로 선언한 필드에 &lt;code&gt;ArrayList&lt;/code&gt;를 넣는 경우나 제네릭 타입 인자로 들어간 스레드 안전하지 않은 타입은 잡지 못합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;상위 타입에만 애너테이션이 있고 실제 선언 타입에는 없는 경우도 별도로 계층을 탐색하지 않으면 놓칩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;필드 접근을 외부 lock으로 보호하는지나 컨트롤러에 별도 scope가 붙었는지도 이 규칙은 판단하지 않습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;애너테이션이 없는 JDK 타입은 두 번째 규칙처럼 목록을 직접 관리해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;규칙은 &lt;code&gt;@RestController&lt;/code&gt;가 붙은 클래스에 직접 선언된 필드만 봅니다. 그래서 애너테이션이 없는 상위 클래스에서 상속한 필드도 놓칩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java에서 어떤 클래스가 멀티스레드에서 의도하지 않게 쓰일 때 그 부작용은 심각하고, 문제가 생긴 곳을 추적하기도 어렵습니다. 그렇기 때문에 스레드 안전성은 문서에 분명하게 적어야 합니다. 그러나 Javadoc 설명문에 적는 방식은 클래스마다 위치와 표현이 제각각이고, 사람이 주의 깊게 읽어야만 효과가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스레드 안전성을 애너테이션으로 표시하면 위치와 형식이 일정해지고 도구가 읽을 수 있습니다. Error Prone의 애너테이션이나 Apache 라이선스로 재구현된 JCIP 애너테이션을 권합니다. Error Prone은 &lt;code&gt;@GuardedBy&lt;/code&gt;와 &lt;code&gt;@Immutable&lt;/code&gt; 위반을 컴파일 오류로 막고, IntelliJ IDEA는 편집기 경고로, SpotBugs는 분석 보고서로 일부 위반을 알려 줍니다. 다만 &lt;code&gt;@ThreadSafe&lt;/code&gt;로 선언한 계약은 세 도구 모두 기본 설정에서 검증하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스레드 안전성을 위해 프로젝트별로 정한 규칙은 ArchUnit으로 검사하기를 권합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;책&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/effective-java-3rd/9780134686097/&quot;&gt;Effective Java, Third Edition&lt;/a&gt;: Item 82. Document thread safety&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jcip.net/&quot;&gt;Java Concurrency in Practice&lt;/a&gt;: 4.5 Documenting synchronization policies, Appendix A. Annotations for Concurrency&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;애너테이션 라이브러리&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://jcip.net/annotations/doc/net/jcip/annotations/package-summary.html&quot;&gt;JCIP annotations Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/stephenc/jcip-annotations&quot;&gt;JCIP Annotations under Apache License&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://errorprone.info/api/latest/com/google/errorprone/annotations/package-summary.html&quot;&gt;Error Prone annotations Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/google/guava/issues/2960&quot;&gt;Guava issue #2960: Migrate off of jsr305&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Javadoc 사례&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.3.6&quot;&gt;JLS 25: synchronized Methods&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedList.html&quot;&gt;JDK 25 LinkedList Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Collections.html#synchronizedList(java.util.List)&quot;&gt;JDK 25 Collections.synchronizedList Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ConcurrentModificationException.html&quot;&gt;JDK 25 ConcurrentModificationException Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/text/SimpleDateFormat.html&quot;&gt;JDK 25 SimpleDateFormat Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/format/DateTimeFormatter.html&quot;&gt;JDK 25 DateTimeFormatter Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8198249&quot;&gt;JDK-8198249: Remove deprecated Runtime::runFinalizersOnExit and System::runFinalizersOnExit&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/file/FlatFileItemWriter.html&quot;&gt;Spring Batch FlatFileItemWriter Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://hc.apache.org/httpcomponents-core-5.3.x/current/httpcore5/apidocs/org/apache/hc/core5/annotation/ThreadingBehavior.html&quot;&gt;HttpCore 5 ThreadingBehavior Javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/apache/httpcomponents-client/tree/rel/v5.5/httpclient5/src/main/java/org/apache/hc/client5/http/impl&quot;&gt;HttpClient 5.5 소스&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/apache/httpcomponents-client/tree/rel/v4.5.2/httpclient/src/main/java/org/apache/http&quot;&gt;HttpClient 4.5.2 소스&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://issues.apache.org/jira/browse/HTTPCLIENT-1743&quot;&gt;HTTPCLIENT-1743: Migrate from CC-BY licensed source&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/apache/httpcomponents-core/blob/4.4.x/RELEASE_NOTES.txt&quot;&gt;HttpCore 4.4.x Release Notes&lt;/a&gt;: Release 4.4.5&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;정적 분석 도구&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SpotBugs&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html&quot;&gt;SpotBugs Bug descriptions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://spotbugs.readthedocs.io/en/stable/eclipse.html&quot;&gt;SpotBugs Eclipse plugin&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Error Prone&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://errorprone.info/bugpattern/GuardedBy&quot;&gt;Error Prone: GuardedBy&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://errorprone.info/bugpattern/Immutable&quot;&gt;Error Prone: Immutable&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SonarQube&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/SonarSource/sonar-java/blob/9bf31b6e003782152b920fac0b1cfa5ba55e05a1/java-checks/src/main/java/org/sonar/java/checks/VolatileNonPrimitiveFieldCheck.java&quot;&gt;SonarJava: S3077 구현 (9bf31b6e0037)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/importing-external-issues/external-analyzer-reports&quot;&gt;SonarQube: External analyzer reports&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IntelliJ&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/Java-Concurrency-annotation-issues.html&quot;&gt;IntelliJ IDEA Inspectopedia: Concurrency annotation issues&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/FieldAccessNotGuarded.html&quot;&gt;IntelliJ IDEA Inspectopedia: Unguarded field access or method call&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/NonFinalFieldInImmutable.html&quot;&gt;IntelliJ IDEA Inspectopedia: Non-final field in @Immutable class&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/AccessToStaticFieldLockedOnInstance.html&quot;&gt;IntelliJ IDEA Inspectopedia: Access to static field locked on instance&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/intellij-community/blob/826413b22cfe5b5c573662f5d2442f454dbc0b23/java/java-psi-api/src/com/intellij/codeInsight/ConcurrencyAnnotationsManager.java&quot;&gt;IntelliJ Community: ConcurrencyAnnotationsManager (826413b22cfe)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/qodana/about-qodana.html&quot;&gt;About Qodana&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/qodana/quality-gate.html&quot;&gt;Qodana Quality gate&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://help.eclipse.org/latest/topic/org.eclipse.jdt.doc.isv/guide/jdt_api_options.htm&quot;&gt;Eclipse JDT Core Options&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/factory-scopes.html&quot;&gt;Spring Framework Bean Scopes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.archunit.org/userguide/html/000_Index.html&quot;&gt;ArchUnit User Guide&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;주요 변경이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2026.09.05&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;정적 검사 도구와 IDE 지원 범위 조사 보강&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2026.09.03&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;제목과 절 구조를 개편&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;대상 라이브러리 최신화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ArchUnit 검사 추가&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2012.05.30&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;최초 작성&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>최상위권 n등의 점수 구하기</title>
      <link>https://blog.benelog.net//2907090.html</link>
      <pubDate>Wed, 29 Feb 2012 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2907090.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.benelog.net/2173103&quot;&gt;Effective &amp;amp; Agile Java Generics&lt;/a&gt; 글에 이은, Generics 활용 사례입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;배치 프로그램을 짜다가 수십만개의 점수 중에 상위 0.1%, 0.01%에 해당하는 값이 무엇인지 구해야하는 일이 생겼습니다. 기존의 프로그램은 Linux shell로 되어 있었는데, Linux의 sort를 이용해서 전체 정렬을 한 후에 찾고자하는 등수가 있는 줄을 찾아가는 방식이였습니다. 전체 건수에 비해서 구하고자 하는 등수가 상위 0.1%, 0.01% 등 아주 작은 비율임에도 불구하고, 전체 Sort를 하는 것은 비효율적이라는 생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 이 프로그램의 일부를 Java로 바꾸는 작업을 하다가 이 순위를 구하는 부분도 필요한만큼만 정렬을 하도록 바꾸고 싶었습니다.  저는 OS를 우분투를 쓰기 떄문에 상관없지만, Java로 만든 프로그램에서 중간에 Shell을 호출하는 부분이 남아있으면 다른 개발자들은 Local PC에서는 확인할 수 없는 부분이 생겨서 불편해질까봐 걱정되었던 이유도 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;등수를 구하는 대상 데이터형은 Integer와 Float이였지만, Integer를 위한 클래스 따로 만들고 Float를 위한 것을 따로 만들기 싫어서 java.lang.Comparable를 구현한 클래스명을 다 사용할 수 있게 했습니다. 이정도 유연성을 두는게 추가 개발은 거의 없이 그냥 Generics 선언을 잘하면 되는 일이였으므로 그다지 과도한 확장은 아니였다고 생각했습니다. 덕분에 String형의 문자열도 비교할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉, 문제를 다시 정리하면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;데이터는 점수값만이 1차원으로 나열된다. (예: 1,5,3,10 &amp;#8230;&amp;#8203;. )&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;java.lang.Comparable형의 클래스를 모두 계산 대상으로 받을수 있고, 명시적인 캐스팅이 필요없게 사용할 수 있어야한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;전체 데이터 중에 지정한 등수를 구한다. 전체 데이터는 파일에 있고, 수십만건이라서 메모리에 모두 올릴 수 없으나, 지정한 등수까지는 메모리에 올릴 수 있을 정도로 최상위권의 값이 지정된다. 즉 예를 들면 80만건중 100등의 점수가 몇점인지를 구하는 상황이다&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Integer, Float, String형을 대상으로 작성한 테스트 코드는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public void get3rdRankOf5() {
    //given
    List&amp;lt;Integer&amp;gt; rawData = asList(400,400,100,500,200);
    int rankToFind = 3;

    //when
    RankFinder&amp;lt;Integer&amp;gt; rankFinder = new RankFinder&amp;lt;Integer&amp;gt;(rankToFind);
    addAll(rawData, rankFinder);
    Integer rankedValue = rankFinder.getRankedValue();

    //then
    assertThat(rankedValue, is(400));
    assertThat(rankFinder.getTopValues(), is(asList(400,400,500)));
}

@Test
public void get2ndRankOf5WithFloat() {
    //given
    List&amp;lt;Float&amp;gt; rawData = asList(10.9F, 10.2F, 10.3F, 10.4F, 10.5F);
    int rankToFind = 2;

    //when
    RankFinder&amp;lt;Float&amp;gt; rankFinder = new RankFinder&amp;lt;Float&amp;gt;(rankToFind);
    addAll(rawData, rankFinder);
    Float rankedValue = rankFinder.getRankedValue();

    //then
    assertThat(rankedValue, is(10.5F));
    assertThat(rankFinder.getTopValues(), is(asList(10.5F,10.9F)));
}

@Test
public void get4thOfWord() {
    //given
    List&amp;lt;String&amp;gt; rawData = asList(&quot;e&quot;,&quot;fc&quot;,&quot;a&quot;,&quot;b&quot;,&quot;k&quot;);
    int rankToFind = 2;

    //when
    RankFinder&amp;lt;String&amp;gt; rankFinder = new RankFinder&amp;lt;String&amp;gt;(rankToFind);
    addAll(rawData, rankFinder);
    String rankedValue = rankFinder.getRankedValue();

    //then
    assertThat(rankedValue, is(&quot;fc&quot;));
}

private &amp;lt;T&amp;gt; void addAll(List&amp;lt;T&amp;gt; rawData, RankFinder&amp;lt;? super T&amp;gt; rankFinder) {
    for (T num : rawData) {
        rankFinder.addTargetValue(num);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이를 통과시키는 실행 코드와 테스트 코드 전체는 &lt;a href=&quot;https://gist.github.com/1938477&quot;&gt;GIST&lt;/a&gt;에 올렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;더 효율적으로 개선할 여지가 많은 방식이지만, 1시간이 걸리는 전체 배치 중 1초 미만의 구간이였기에 속도향상이 큰 의미가 없어서 더 이상 리팩토링을 진행하지는 않았습니다.  알려진 selection alorithm을 참고하면 이 상황에서도 더 좋은 방식이 있을 듯합니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring 3.1을 활용한 AspectJ 표현식  테스트</title>
      <link>https://blog.benelog.net//2893630.html</link>
      <pubDate>Wed, 18 Jan 2012 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2893630.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;AOP에서 AspectJ표현식은 다양하고 강력한 기능을 제공합니다.
예를 들면 Aspect가 결합될 JoinPoint를 아래와 같이 표현을 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;execution(public void set*(..))&lt;/code&gt; : 리턴 타입이 void이고 메서드 이름이 set으로 시작되고 파라미터가 0개 이상인 메서드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;execution(* com.benelog.ftpserver.&lt;strong&gt;.&lt;/strong&gt;()&lt;/code&gt; : com.benelog.ftpserver패키지의 파라미터가 없는 모든 메서드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;execution(String com.benelog.ftpserver..&lt;strong&gt;.&lt;/strong&gt;(..)&lt;/code&gt; : 리턴타입이 String이면서 com.benelog.ftpserver패키지 및 하위 패키지에 있는 파라미터가 0개 이상인 메서드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;execution(String com.benelog.ftpserver.Server.insert(..)&lt;/code&gt; : 리턴타입이 String인 com.benelog.ftpserver.Server의 파라미터가 0개 이상인 메서드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;execution(* get*(*))&lt;/code&gt; : get으로 이름이 시작하고 1개의 파라미터를 갖는 메서드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;execution(* get*(&lt;strong&gt;,&lt;/strong&gt;))&lt;/code&gt; : get으로 이름이 시작하고 2개의 파라미터를 갖는 메서드 exceution(* read*(Interger,&amp;#8230;&amp;#8203;)) : read로 이름이 시작하고, 첫번째 파라미터 타입이 Integer이며, 1개 이상의 파라미터를 갖는 메서드&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 다양하고 강력한 대신에 모든 문법을 다 익숙하게 쓰기는 힘들고, 컴파일 타임에 체크도 안 되기 때문에 실수할 여지도 큽니다. 조금이라도 표현식을 바꾸었을 때 유지하고 싶은 기존의 동작이 변함없이 작동되는지도 파악하기 어렵습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 AOP는 강력한만큼  통제되지 못한 부작용이 생기기도 쉽습니다. 표현식 한글자를 바꾸어도 어떤 동작을 하는지 테스트를 촘촘히 해봐야하는데, 그럴 때마다 WAS를 띄워서 테스트를 해본다면 시간이 많이 들어갈 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring 3.1부터는 테스트 코드 안에서 &lt;code&gt;@Configuration&lt;/code&gt; , &lt;code&gt;@Bean&lt;/code&gt; 과 같은 JavaConfig 설정들이 지원됩니다.
그 기능을 이용하면 XML없이 보다 짧은 코드로 AOP에 대한 다양한 테스트를 편하게 해 볼 수 있습니다.
단순한 예제로 Spring 3.1의 기능으로 AOP 테스트하는 방식을 정리해보았습니다.
아래 설명한 예제들은 gist에 올라가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/1625374&quot; class=&quot;bare&quot;&gt;https://gist.github.com/1625374&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;logaspect_java&quot;&gt;LogAspect.java&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;LogAspect.java는 간단하게 로그를 저장소에 입력하는 Aspect입니다.
&lt;code&gt;@Before(&quot;execution(void *.run())&quot;)&lt;/code&gt; 라는 표현식으로 run 메서드가 실행되기 전에 로그메시지를 저장하도록 taget 객체와 결합됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Aspect
public class LogAspect {
    private LogRepository repository;

    public LogAspect(LogRepository repository){
        this.repository = repository;
    }

    @Before(&quot;execution(void *.run())&quot;)
    public void log(JoinPoint jp){
        String message = jp.getKind() + &quot;:&quot; + jp.getSignature().getName();
        repository.insertLog(message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;logrepository_java&quot;&gt;LogRepository.java&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로그저장소 역할을 하는 클래스입니다. 실무에서라면 DB나 파일 등을 사용하겠지만, 여기서는 단순한 예제를 만들고 싶어서 그냥 String을 화면에 찍었습니다. 정교하게 만든다면 Log정보를 담는 Domain Object를 정의해서 String대신 사용해야 할 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class LogRepository  {
    public void insertLog(String message) {
        System.out.println(message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;taget_클래스&quot;&gt;Taget 클래스&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;LogAspect와 결합될 Target 클래스입니다. 역시나 단순하게 화면에 메시지를 뿌려줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class Printer implements Runnable {
    @Override
    public void run() {
        System.out.println(&quot;hello!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;실제_코드에서의_applicationcontext_설정&quot;&gt;실제 코드에서의 applicationContext 설정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;applicationContext를 설정하는 xml파일에서   aop: 네임스페이스를 이용해서 aspect J 표현식을 쓸 수 있게 하고, Aspect가 정의된 LogAspect를 bean으로 등록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&amp;lt;aop:aspectj-autoproxy/&amp;gt;
&amp;lt;bean id=&quot;printer&quot; class=&quot;edu.tdd.aop.Printer&quot;/&amp;gt;
&amp;lt;bean id=&quot;logAspect&quot; class=&quot;edu.tdd.aop.LogAspect&quot;&amp;gt;
    &amp;lt;constructor-arg&amp;gt;
        &amp;lt;bean class=&quot;edu.tdd.aop.LogRepository&quot;/&amp;gt;
    &amp;lt;/constructor-arg&amp;gt;
&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;logaspecttest_java&quot;&gt;LogAspectTest.java&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;드디어 LogAspect의 표현식을 테스트하는 코드입니다. @Bean으로 Aspect와 tagetObject를 모두 XML설정 대신 java로 한 파일 안에서 표현했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 테스트 메서드별로  2가지를 검증했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(1) proxyCreated() 메서드 : Proxy클래스가 생성되었는지 확인
(2) logInserted 메서드: LogAspect에서 호출하는 LogRepository에 원하는 메시지가 저장되었는지 확인&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class LogAspectTest {
    @Autowired Runnable targetProxy;
    @Autowired LogRepository repository;

    @Test
    public void proxyCreated() {
        assertTrue(AopUtils.isAopProxy(targetProxy));
    }

    @Test
    public void logInserted() {
        targetProxy.run();
        verify(repository).insertLog(&quot;method-execution:run&quot;);
    }

    @Configuration
    @EnableAspectJAutoProxy
    static public class TestContext {
        @Bean LogAspect aspect() {
            return new LogAspect(repository());
        }

        @Bean LogRepository repository(){
            return mock(LogRepository.class); // 검증용 객체를 application에다 등록
        }

        @Bean Runnable target(){
            return new Printer();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;클래스 선언부분에서 @ContextConfiguration(loader=AnnotationConfigContextLoader.class) 를 붙이면 외부의 XML 대신 테스트 클래스 안에 포함된 JavaConfig로된 설정을 읽어올 수 있습니다. TestContext라는 내부 클래스에다 @Configuration을 붙여서 필요한 Bean을 등록했습니다.
&lt;code&gt;@EnableAspectJAutoProxy&lt;/code&gt; 는 &amp;lt;aop:aspectj-autoproxy/&amp;gt;와 같은 역할을 하는 애너테이션입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 ApplicationContext에 LogRepository는 mock()으로 생성한 가짜 객체를 등록했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Bean LogRepository repository(){
        return mock(LogRepository.class); // 검증용 객체를 application에다 등록
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 객체를 다시 Autowired로 받아온 후에 verify()를 했습니다.   XML로 이와 비슷한 일을 하려면 코드가 더 길어지고, 별도의 파일로 분리가 됩니다. Mock을 Application에 등록하는 것이 바람직할지는 고민의 여지가 있지만 Aspect와 결합된 Proxy 클래스는 ApplicationContext를 통해야만 얻을 수 있고, 그 동작을  검증하고자 한다면 이 방식이 편하다고 느껴집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서는 Printer객체를 target클래스로 사용했는데, 이 부분도 실제로 원하는 AOP 적용을 원하는 클래스나 제외 되어야할 클래스, 또는  직접 만든 테스트 전용 객체 등을 바꿔끼워가면서 다양한 조건을 검증할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;덧붙여 &lt;code&gt;verify(repository).insertLog(&quot;method-execution:run&quot;);&lt;/code&gt; 부분에서 문자열을 직접 검증하는 것도 바람직하지 못한 방식일지도 모릅니다.  문자열 대신에 Log정보를 담는 도메인 객체를 따로 정의했다면, 그 객체에 메시지에 핵심키를 담고, equals를 override했을 것 같습니다.
문자열을 직접 검증하면 메시지의 형식이 바뀔 때마다 테스트가 깨어져서 유지보수가 번거로운 테스트 코드가 됩니다. 이 예제에서는 코드를 짧게 하고 테스트 코드 안에서 검증하고자 하는 동작을 단순하게 나타내려고 이 방식을 택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Local 개발환경에서 WAS를 띄우는 여러가지 방법</title>
      <link>https://blog.benelog.net//2879657.html</link>
      <pubDate>Tue, 6 Dec 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2879657.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Local 개발환경에서 WAS를 띄우는 여러가지 방식과 장단점에 대해서 정리해 봤습니다.  저는 상황에 따라서 아래 3가지 방법들을 그때 그때 골라서 씁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;Eclipse WTP (+Agent based reloading) : WAS를 올린 상태에서 클래스파일을 고칠 때, class파일 1개만 리로딩하는 것이 좋을 때&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Maven Jetty plugin : JSP만 고칠 때나 클래스 파일을 고치면 전체 애플리케이션을 reloading을 하는 것이 유리할 때.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Maven Tomcat plugin : JSP만 고칠때. WTP가 뭔가 꼬인 것 같은데 같은 Tomcat에서 같은 에러가 나는지 확인해보고 싶을 때&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;1_eclipse_wtpweb_tools_platform_agent_based_reloading&quot;&gt;1. Eclipse WTP(Web Tools Platform) + Agent Based Reloading&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Maven 프로젝트가 &lt;code&gt;&amp;lt;packaging&amp;gt;war&amp;lt;/packaging&amp;gt;`로 선언된 경우 `mvn eclipse:eclipse&lt;/code&gt; 명령을 치면 Eclipse의 dynamic web project로 설정을 만들어 줍니다.  Run as Server혹은 server 설정에 drag &amp;amp; drop으로 넣고 Eclipse안에서 서버를 실행합니다.
M2Eclipse를 잘 활용하면 local에서는 수동으로 maven 빌드를 돌릴 일이 거의 없어지기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;항상 쓰는 것은 아니지만, SpringSource  Tool Suite의 Agent based reloading을 이용해서 class파일을 수정하고 빠르게 리로딩을 하기도 합니다. 모든 상황에서 잘 통하지는 않아서, 그냥 WTP만 쓰는 것이 나을 때도 있습니다. 예를 들면 XML파일을 고쳤을 때에는 Agent based reloading에서는 잘 반영되지 않았습니다. JRebel에서는 보다 다양한 경우를 지원하는 것 같지만, 저는 테스트 코드에서 View를 빼놓고는 왠만한 건 실행해보고 WAS를 올리기 때문에 보다 강력한 JRebel이 크게 절실하지는 않습니다&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SpringSource Tool Suite의 Java Agent based reloading 사용해 보기 : &lt;a href=&quot;http://blog.benelog.net/2796964&quot; class=&quot;bare&quot;&gt;http://blog.benelog.net/2796964&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, Eclipse의 Dynamic web project + WTP 환경에서는 JSP를 고칠때의 반영속도가 미묘하게 느린 것 때문에  web context 폴더를 소스폴더 외에도 별도로 잡고, /WEB-INF/classes나 lib까지 다 그 폴더로 복사해서 &apos;add external web module&amp;#8217;로 등록해서 쓰시는 분들도 많습니다. 어디가 소스폴더이고 어디가 목적폴더인지  정리되어 있지 않아서 WEB-INF/lib나 WEB-INF/classes같이 꼭 버전관리가 필요없는 파일까지 같이 SVN에 commit하기도 하고, 복잡하게 svn:ignore를 시키는 수고를 하기도 합니다.
WTP에서 &apos;Serve modules without publishing &apos; 옵션을 써서 이를 해결하기도 합니다.
이에 대한 자세한 내용은 아래 자료에 정리되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.google.com/document/d/1fYoWD_0-3sGxHjHNHKgGNLmONVTQ9DmaLwO-MSXVCHU/edit&quot; class=&quot;bare&quot;&gt;https://docs.google.com/document/d/1fYoWD_0-3sGxHjHNHKgGNLmONVTQ9DmaLwO-MSXVCHU/edit&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음에 소개하는 maven jetty plugin과 Jetty나 Maven plugin을 써도 JSP가 더 빠르게 반영되기 때문에, WTP의 JSP반영 속도 때문에 일부러 &apos;add external modules&amp;#8217;로 WAS를 띄울 필요성은 적어집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;2_maven_jetty_plugin&quot;&gt;2. Maven jetty plugin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;pom.xml에 Maven jetty plugin을 설정하면 따로 별도의 WAS설치과정이 필요없이 mvn jetty:run만 치면 바로 WAS가 뜹니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;org.mortbay.jetty&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;maven-jetty-plugin&amp;lt;/artifactId&amp;gt;
     &amp;lt;configuration&amp;gt;
         &amp;lt;scanIntervalSeconds&amp;gt;3&amp;lt;/scanIntervalSeconds&amp;gt;
         &amp;lt;contextPath&amp;gt;/&amp;lt;/contextPath&amp;gt;
      &amp;lt;/configuration&amp;gt;
       &amp;lt;version&amp;gt;6.1.11&amp;lt;/version&amp;gt;
&amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;디폴트로 8080포트를 쓰려면 위에서 &quot;connector&apos; 부분의 설정이 없어도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래의 Tomcat plugin도 마찬가지지만, 이런 plugin이 설정되어 있으면, Eclipse가 없어도 SVN에서 checkout한후바로 명령어 하나로 WAS를 띄워볼 수 있습니다. 그 프로젝트의 개발자가 아닌 사람이 프로젝트를 띄워보거나 가끔 들어가는 프로젝트를 실행할 때도 편합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대부분의 웹애플리케이션은 Servlet Spec만 따라서 개발하므로 Jetty에서 돌아가면 Tomcat에서 거의 돌아간다고 봐도 됩니다. 물론 성능테스트나 프로파일링 같은건 당연히 Tomat을 띄워서 해야겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음에 소개할 Maven Tomcat plugin보다 WAS가 뜨는 속도나 리로딩 속도도 빠르다는 느낌입니다.
&quot;scanIntervalSeconds&quot; 설정에 따라서 클래스 파일을 고쳐도 리로딩이 잘 됩니다.
&apos;WTP + Agent based reloading&amp;#8217;을 쓴다고 해도 뭔가 꼬이는 상황이 발생할 수 있고, 그럴 때는 전체 web context를 로딩하는게 좋습니다.
그런 상황이 많은 개발환경이라면 Jetty plugin이 유리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;단점은 Tomcat이 아닌 Jetty이니 WAS level의 클래스가 던지는 에러메시지가 약간 다르기도 하고, Apache와 연결할때 AJP connector등을 설정할 수 없어서 Apache httpd의 모듈을 사용하는 페이지에는 쓸 수 없다는 점입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;3_maven_tomcat_plugin&quot;&gt;3. Maven Tomcat plugin&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jetty plugin과 비슷하게 pom.xml에 Tomcat plugin을 설정하고, &lt;code&gt;mvn tomcat:run&lt;/code&gt; 을 치면 WAS 설치가 필요없이 Tomcat이 뜹니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설정은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;tomcat-maven-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.1&amp;lt;/version&amp;gt;
    &amp;lt;configuration&amp;gt;
        &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
        &amp;lt;serverXml&amp;gt;$\{basedir}/src/main/webapp/server.xml&amp;lt;/serverXml&amp;gt;
    &amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;만약 AJP설정 같은게 필요없어서 server.xml을 지정할 것까지도 없고 포트만 지정하면 된다면 아래처럼 하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;configuration&amp;gt;
    &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
    &amp;lt;port&amp;gt;8080&amp;lt;/port&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 port도 지정할 필요가 없이 디폴트인 8080으로 띄우고 싶다면 &quot;&amp;lt;port/&amp;gt;&quot; 부분도 생략해도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;장점은 Tomcat이니까 다른 개발서버, 운영서버와 똑같은 환경이고, 같은 에러메시지가 뜬다는 점입니다. 그리고 server.xml을 지정할 수 있으니 AJP연결등으로 Apache와 연동하는 개발에도 쓸 수 있습니다.  WTP가 꼬인 것 같을 때 mvn tomcat:run으로 확인해보고 다시 Eclipse의 WTP로 돌아가기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 Plugin은 org.codehaus.mojo의 아래에 존재했지만, 최신 버전은 아래 링크의 Apache Tomcat 프로젝트 산하로 들어갔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://tomcat.apache.org/maven-plugin-2.0/tomcat6-maven-plugin/&quot; class=&quot;bare&quot;&gt;http://tomcat.apache.org/maven-plugin-2.0/tomcat6-maven-plugin/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://tomcat.apache.org/maven-plugin-2.0/tomcat7-maven-plugin/&quot; class=&quot;bare&quot;&gt;http://tomcat.apache.org/maven-plugin-2.0/tomcat7-maven-plugin/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tomcat7은 아래와 같이 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;    &amp;lt;plugin&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.tomcat.maven&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;tomcat7-maven-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.2&amp;lt;/version&amp;gt;
        &amp;lt;configuration&amp;gt;
            &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
        &amp;lt;/configuration&amp;gt;
    &amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;mvn tomcat7:run&lt;/code&gt; 으로 실행할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마찬가지로 Tomcat6 plugin도 활용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;    &amp;lt;plugin&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.tomcat.maven&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;tomcat6-maven-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.2&amp;lt;/version&amp;gt;
        &amp;lt;configuration&amp;gt;
            &amp;lt;path&amp;gt;/&amp;lt;/path&amp;gt;
        &amp;lt;/configuration&amp;gt;
    &amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;mvn tomcat6:run&lt;/code&gt; 으로 실행합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 spring-loaded를 함께 쓰면 Tomcat재시작 횟수를 줄일수 있습니다.
( &lt;a href=&quot;https://gist.github.com/benelog/aee89ac5b6ff896b2e0f&quot; class=&quot;bare&quot;&gt;https://gist.github.com/benelog/aee89ac5b6ff896b2e0f&lt;/a&gt; 참조 )&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Winstone, 경량 Servlet container</title>
      <link>https://blog.benelog.net//2875999.html</link>
      <pubDate>Fri, 25 Nov 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2875999.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Winstone(&lt;a href=&quot;http://winstone.sourceforge.net/&quot; class=&quot;bare&quot;&gt;http://winstone.sourceforge.net/&lt;/a&gt; )은 jar파일 하나로 실행되는 간단한 Servlet Container입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FTP처럼 서버에 있는 파일을 다운로드 할 때나, 웹애플리케이션을 jar파일로 패키징해서 WAS 설치없이 독립적으로 실행되도록 만드는 용도로 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로, Hudson에서도 winstone을 활용하고 있습니다.  hudson.war를 WAS에 설치하지 않아도  java -jar 로 바로 실행시킬 수도 있는데, 이 기능이 Hudson 패키징 파일 안에 내장된 winstone을 이용하는 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 winstone을 간단하게 활용하는 방법을 정리해봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ftp대용으로_쓰기&quot;&gt;FTP대용으로 쓰기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 URL에서 winstone의 jar파일이 제공됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://sourceforge.net/projects/winstone/files/&quot; class=&quot;bare&quot;&gt;http://sourceforge.net/projects/winstone/files/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 접근하기 편한 URL에 올려놓고 wget 으로 다운로드하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;wget benelog.net/winstone.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;받은 파일을 &lt;code&gt;java -jar&lt;/code&gt; 로 간단하게 실행하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 디렉터리를 최상위 폴더로 실행하려면 아래와 같이 하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;java -jar winstone.jar --webroot=.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;HTTP 포트를 지정하려면 --httpPort 옵션을 붙이면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;java -jar winstone.jar --webroot=. --httpPort=18080&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 다음 해당서버에 지정된 포트로 접근하면 디렉터리의 파일 목록이 뜹니다. FTP와는 다르게 비밀번호가 없이 접근이 되므로 보안문제가 염려된다면 외부에 공개되지 않은 포트로 띄어야 하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/web-server/winstone.jpg&quot; alt=&quot;winstone&quot; title=&quot;winstone.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그외의 다양한 옵션은 Winstone의 사이트(&lt;a href=&quot;http://winstone.sourceforge.net/&quot;&gt;http://winstone.sourceforge.net/&lt;/a&gt; )에 자세히 나와 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;웹애플리케이션을_jar파일_하나로_실행되도록_패키징&quot;&gt;웹애플리케이션을 jar파일 하나로 실행되도록 패키징&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞선 hudson의 사례처럼, 별도의 WAS설치 없이 웹애플리케이션 배포 파일 자체에 WAS를 내장하는 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jetty를  애플리케이션 안에서 직접 심어서(Embedding) 실행시켜도 같은 효과가 있습니다. 제가 간단하게 만들었던, &lt;a href=&quot;http://blog.benelog.net/2874354&quot;&gt;dumper&lt;/a&gt;라는 도구에서도 그 방식을 활용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/dumper/blob/master/src/main/java/Start.java&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/dumper/blob/master/src/main/java/Start.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jetty를 war파일 안에 패키징해서 독립적으로 실행가능한 방식도 있습니다. 아래 자료에 설명되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://eclipsesource.com/blogs/2009/10/02/executable-wars-with-jetty/&quot;&gt;http://eclipsesource.com/blogs/2009/10/02/executable-wars-with-jetty/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.jowisoftware.de/blog/archives/26-Creating-runnable-wars-with-Maven-and-Jetty.html&quot;&gt;http://www.jowisoftware.de/blog/archives/26-Creating-runnable-wars-with-Maven-and-Jetty.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 위의 방식은 따로 실행 시작점이 되는 클래스를 만들어줘야하고, maven설정을 다소 많이 고쳐야하는 단점이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이에 반해서 winstone은 소스파일은 하나도 건드릴 필요없이, maven 설정도 건드리지 않거나 약간만 추가해서 혼자서 실행되는 jar파일을 만들어줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아무런 설정이 없어도 아래와 같이 실행하면 taget 폴더 아래에 .jar파일을 만들어 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;mvn net.sf.alchim:winstone-maven-plugin:embed&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 pom.xml에 아래 설정을 추가하면 &apos;mvn package&amp;#8217;로 패키징을 할 때마다 war파일과 함께 독립실행 가능한 jar파일도 만들어줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;      &amp;lt;plugin&amp;gt;
        &amp;lt;groupId&amp;gt;net.sf.alchim&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;winstone-maven-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.2&amp;lt;/version&amp;gt;
        &amp;lt;executions&amp;gt;
          &amp;lt;execution&amp;gt;
            &amp;lt;phase&amp;gt;package&amp;lt;/phase&amp;gt;
            &amp;lt;goals&amp;gt;
              &amp;lt;goal&amp;gt;embed&amp;lt;/goal&amp;gt;
            &amp;lt;/goals&amp;gt;
          &amp;lt;/execution&amp;gt;
        &amp;lt;/executions&amp;gt;
      &amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 만든 예제인  &lt;a href=&quot;http://blog.benelog.net/2864739&quot;&gt;간단하게 서버에 파일을 올리는 프로그램&lt;/a&gt;에서도 이 방식을 썼습니다. 아래의 pom.xml에 이 설정에 포함되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/uploader/blob/master/pom.xml&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/uploader/blob/master/pom.xml&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 프로젝트를 받고서 mvn package를 하면 target 폴더 아래에 uploader.jar가 생기는데 &lt;code&gt;java -jar uploader.jar&lt;/code&gt; 만 하면 웹애플리케이션이 실행되는 것이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;어디에_쓸까&quot;&gt;어디에 쓸까?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;직접 서비스 운영과 서버관리를 담당하는 일반적인 웹애플리케이션에서는 Winstone을 쓸 일이 그다지 많아보이지는 않습니다. 다만 Huson처럼 패키징해서 배포하고, 여러 가지 옵션을 제공하는 패키지성 웹애플리케이션에서는 고려해볼만도 합니다. 유사하게 Lucene바탕의 API서버인 Solr(&lt;a href=&quot;http://lucene.apache.org/solr/)에서는&quot; class=&quot;bare&quot;&gt;http://lucene.apache.org/solr/)에서는&lt;/a&gt; Jetty를 이용한 stand-alone 실행모드를 제공합니다. 그리고 간단히 소수의 인원이 쓰는 관리도구나, Desktop application 성격의 프로그램이라도 Swing, AWT대신 Web의 UI 기술을 이용하고 싶을 때도 Winstone을 포함한 패키징을 고려해볼만도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;하지만 비슷한 용도의 Jetty와 비교해볼 때  winstone이  유망하다고 생각하지는 않습니다.  2008년도 이후에는 프로젝트 업데이트가 안 되고 있어서 앞으로의 전망이 어두워보입니다. 계속 이런 상태라면 Hudson에서도 언젠가는 Jetty로 갈아타지 않을까하는 생각도 듭니다. 별도로 클래스를 만들지 않아도 되는 장점등도 언젠가는 Jetty에서도 제공될수 있어 보입니다. 그리고 jsp를 쓰는 프로젝트에서는 따로 라이브러리를 지정해야 하는 것도 다소 번거롭습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래도 Wistone은 알아두면 서버에서 파일 주고 받을 때라도 유용하게 쓸 수도 있고, 교육 용도의 실습 프로젝트, 취미용 프로젝트에도 편하게 활용해볼만한 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java Stack dump 다운로드를 편하게</title>
      <link>https://blog.benelog.net//2874354.html</link>
      <pubDate>Sun, 20 Nov 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2874354.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실행 중인 Java 프로세스의 Stack dump를 뜨는 작업은 간단합니다.  Jstack 이나 kill -3 뒤에 process id를 지정하기만 하면 됩니다. Stack dump를 서버에 있는 stack dump파일을 PC로 옮길때면 Secure CRT나 putty에서 console에 찍히는 내용을 파일로 저장하는 기능을 주로 쓰고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;언젠가 테스트를 한다고 자주 Stack dump를 뜨다보니 이런 작업마저도 번거롭게 느껴졌습니다. 그리고  파일이름에 timestamp가 들어가 있으면 나중에 찾아보기가 편한데, 그런 파일명 지정도 수동으로 하려니까 귀찮았습니다. 그래서 간단한 프로그램을 만들어 봤었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보통은 서버에서 vi로 덤프를 보는 때가 많아서 저도 잘 쓰지는 않는데, 그래도 코딩하면서는 재밌었던 기억이 나네요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;다운로드&quot;&gt;다운로드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;wget file.benelog.net/dumper.jar&lt;/code&gt; 혹은 웹브라우저로 benelog.net/dumper.jar 접근해서 저장&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실행&quot;&gt;실행&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Windows : &lt;code&gt;java -cp &quot;dumper.jar;%JAVA_HOME%/lib/tools.jar&quot; Start [port number]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Linux : &lt;code&gt;java -cp &quot;dumper.jar:$JAVA_HOME/lib/tools.jar&quot; Start [port number]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Console 창에는 아래 메시지가 찍힙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Usage:

 (Windows)

   Prompt&amp;gt;java -cp &quot;dumper.jar;%JAVA_HOME%/lib/tools.jar&quot; Start [port]

 (Linux)

   Prompt&amp;gt;java -cp dumper.jar:$JAVA_HOME/lib/tools.jar Start [port]

-----------------------------

The port is selected by ramdom.

Web address: http://192.168.0.11:17456

2011-11-20 04:04:32.901:INFO::jetty-7.x.y-SNAPSHOT

2011-11-20 04:04:33.248:INFO::started o.e.j.s.ServletContextHandler\{/,null}

2011-11-20 04:04:34.526:INFO::Started SelectChannelConnector@0.0.0.0:17456&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파라미터로 port번호를 넘겨서 원하는 포트로 웹페이지를 띄울 수가 있는데, 지정된 포트가 없다면 10000번에서 20000번 사이의 포트를 임의로 찍어서 띄어줍니다. 위에 &quot;Web address&quot; 라고 적힌 칸 옆의 주소가 접근할 수 있는 URL입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;사용법&quot;&gt;사용법&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Console창에서 알려주는 주소를 웹브라우저 입력하면 아래처럼 jps의 정보를 보여주는 화면이 뜹니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/my-program/dumper-jps.jpg&quot; alt=&quot;dumper jps&quot; title=&quot;JPS&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;pid를 찍으면 지정한 JVM의 jstack dump를 바로 다운로드할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파일명은 {pid}_{연도}-{일월}-{시분초}.log의 형식으로 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;혹시 덤프파일을 생성하는데 아래 에러 메시지가 뜬다면, 장비에 여러개의 JVM이 설치되어 있으면서 덤프를 뜨려는 JVM 프로세스와 tools.jar를 classpath로 지정한 경로의 JVM이 다르지 않는지 확인해보셔야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;java.lang.UnsatisfiedLinkError: no attach in java.library.path
com.sun.tools.attach.AttachNotSupportedException: no providers installed
        at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:190)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;구현방식&quot;&gt;구현방식&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jps나 Jstack과 유사하게 sun.jvmstat.monitor 패키지의 클래스를 이용한 프로그램입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.java2s.com/Open-Source/Java-Document/6.0-JDK-Modules-sun/jvmstat/sun.jvmstat.monitor.htm&quot; class=&quot;bare&quot;&gt;http://www.java2s.com/Open-Source/Java-Document/6.0-JDK-Modules-sun/jvmstat/sun.jvmstat.monitor.htm&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 jdk의 tools.jar는 따로 배포하면 license에 어긋난다고 알고 있어서 묶어서 패키징하지 않고 classpath로 지정하도록 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전체 소스는 github에 올렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/dumper&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/dumper&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>간단하게 서버에  파일을 올리기 + MockMultipartFile을 이용한 테스트</title>
      <link>https://blog.benelog.net//2864739.html</link>
      <pubDate>Sun, 23 Oct 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2864739.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서버에 1,2개 파일만을 올려야할 때는  FTP를  따로 설치하는 일이 번거롭다고 느껴집니다. 그럴 때  간단하게 다운받아서 실행할 수 있는 웹애플리케이션을 만들어봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jar파일 하나만 다운로드 받아서 바로 실행시키면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;다운로드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;wget file.benelog.net/uploader.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;실행&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;java -jar uploader.jar (디폴트로 8080포트)
java -jar uploader.jar --httpPort=2010 (포트지정)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;div class=&quot;title&quot;&gt;서버를 브라우저로 접속해서 파일을 올리기&lt;/div&gt;
&lt;p&gt;예) &lt;a href=&quot;http://localhost:8080/&quot; class=&quot;bare&quot;&gt;http://localhost:8080/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/my-program/uploader.png&quot; alt=&quot;uploader.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따로 Tomcat과 같은 WAS를 설치할 필요가 없도록 경량 WAS인 Winstone(&lt;a href=&quot;http://winstone.sourceforge.net/&quot; class=&quot;bare&quot;&gt;http://winstone.sourceforge.net/&lt;/a&gt;) 과 함께 패키징했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소스코드는 github에 올려놨습니다. ( &lt;a href=&quot;https://github.com/benelog/uploader/&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/uploader/&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;특별한 코드는 없지만, 아래 클래스에서 &lt;a href=&quot;http://static.springsource.org/spring/docs/2.0.x/api/org/springframework/mock/web/MockMultipartFile.html&quot;&gt;MockMultipartFile&lt;/a&gt;를 이용해서 파일업로드에 대한 테스트코드를  만들면서 나름 재미있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/uploader/blob/master/src/test/java/net/benelog/uploader/UploadControllerTest.java&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/uploader/blob/master/src/test/java/net/benelog/uploader/UploadControllerTest.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Btrace로 DBCP의 connection정보를 모니터링 하기</title>
      <link>https://blog.benelog.net//2855201.html</link>
      <pubDate>Wed, 28 Sep 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2855201.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Connection pool을 library로 DBCP를 많이 사용하고 있습니다. DataSouce가 선언되어 있는 설정 파일을 보면 min, max Connection 숫자는 쉽게 알 있지만, 현재 Active 한 것이 몇개 인지등은 실행시간에 쉽게 알아보기가 쉽지가 않죠. 그래서 JMX로 현재 Active한 Connection 수 등을 노출하는, DBCP를 한번 감싼 DataSource를 사용하기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Btrace를 이용하면 비교적 간단하게 DBCP의 속성들을 값들을 확인할 수 있습니다. 이미 실행되고 있는 JVM에도 붙일 수 있으니 설정을 바꾸고 WAS를 재시작하지 않아도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 간단한 소스로도 가능합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import static com.sun.btrace.BTraceUtils.*;
import java.lang.reflect.Field;
import com.sun.btrace.BTraceUtils.Sys;
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.OnMethod;
import com.sun.btrace.annotations.Self;

@BTrace
public class DbcpMonitorSimple {

  private static final String DS_CLASS = &quot;org.apache.commons.dbcp.BasicDataSource&quot;;

  @OnMethod(clazz = DS_CLASS, method = &quot;getConnection&quot;)
  public static void onGetConnection(@Self Object basicDataSource) {
    Field urlField = field(DS_CLASS, &quot;url&quot;);
    Object url = get(urlField, basicDataSource);
    print(&quot;=====DBCP BasicDataSource info (&quot;);
    print(url);
    println(&quot; ) ==========&quot;);
    printFields(basicDataSource);
    Field poolField = field(DS_CLASS, &quot;connectionPool&quot;);

    Object pool = get(poolField, basicDataSource);
    println(&quot;=====connectionPool (GenericObjectPool) info====&quot;);
    printFields(pool);
    println(&quot;==========&quot;);
    Sys.exit(0);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DataSource.getConnection 요청이 있을 때 해당 객체를 얻어와서 속성값들을 찍어줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 필요한 필드만 찍는다던지, 보기좋게 정렬한다던지 하는 작업은 필요에 따라 하면 되겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;jps로 모니터링하고자 하는 JVM의 pid를 확인하고,
&lt;code&gt;btrace [pid] DbcpSimpleMonitor.java&lt;/code&gt; 로 실행하면 아래와 같이 속성값들을 쭉 찍어줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;gt;btrace 4288 DbcpMonitor.java


=====DBCP BasicDataSource info (jdbc:hsqldb:file:store;shutdown=true ) ========

{defaultAutoCommit=true, defaultReadOnly=null, defaultTransactionIsolation=-1, d

efaultCatalog=null, driverClassName=org.hsqldb.jdbcDriver, driverClassLoader=nul

l, maxActive=8, maxIdle=8, minIdle=0, initialSize=0, maxWait=-1, poolPreparedSta

tements=false, maxOpenPreparedStatements=-1, testOnBorrow=false, testOnReturn=fa

lse, timeBetweenEvictionRunsMillis=-1, numTestsPerEvictionRun=3, minEvictableIdl

eTimeMillis=1800000, testWhileIdle=false, password=, url=jdbc:hsqldb:file:store;

shutdown=true, username=sa, validationQuery=null, validationQueryTimeout=-1, con

nectionInitSqls=null, accessToUnderlyingConnectionAllowed=false, restartNeeded=t

rue, connectionPool=org.apache.commons.pool.impl.GenericObjectPool@1f31079, conn

ectionProperties={user=sa, password=}, dataSource=org.apache.commons.dbcp.Poolin

gDataSource@be8958, logWriter=java.io.PrintWriter@12b1ff9, abandonedConfig=null,

 closed=false, }

===== number of Active : 0

==========&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FIeld 객체를 얻어올 때 field(String,String)을 쓰면 static 필드초기화나 @OnMethod 메서드가 붙지 않은 메서드에서는 객체가 얻어지지 않는  에러가 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;인터넷을 찾아보니 아래와 같이 비슷한 현상을 겪은 사람이 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://kenai.com/projects/btrace/forums/forum/topics/1366-Drilling-into-a-method-argument-s-properties&quot;&gt;http://kenai.com/projects/btrace/forums/forum/topics/1366-Drilling-into-a-method-argument-s-properties&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;결국 &lt;code&gt;@OnMethod&lt;/code&gt; 가 붙지 않는 메서드에서 호출을 할 때는 &lt;code&gt;field(classof(obj), &quot;fieldName&quot;)&lt;/code&gt; 과 같이 피해가는 방법을 썼습니다. 되도록 메서드에서 매번 호출할 필요가 없는 부분은 static 초기화를 시킬 수 있었으면 하는데, static 초기화로는 하는 것도 잘 되지 않았습니다. 직접 field(BasicDataSource.class, &quot;fieldName)으로 를 참조하기는 것도 잘 안 되었고, field(String,String)으로는 참조가 안 되었습니다.
이것만 아니면 좀 더 최적화된 호출을 할 수 있었을 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첨부한 파일들은 아래와 같은 다소 다른 방식이나 출력형식으로 택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/blob/main/examples/btrace-scripts/src/main/java/DbcpMonitorSimple.java&quot;&gt;DbcpMonitorSimple.java&lt;/a&gt; : BasicDataSource.getConnection이 한번 호출될 때 BasicDataSource의 모든 정보와 BasicDataSource.connetionPool의 정보를 출력&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/blob/main/examples/btrace-scripts/src/main/java/DbcpMonitor.java&quot;&gt;DbcpMonitor.java&lt;/a&gt; :  이벤트 방식은 1과 같고, BasicDataSource의 모든 필드와 BasicDataSource.connetionPool에서 _numActive 값만을 보여줌&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/blob/main/examples/btrace-scripts/src/main/java/DbcpActiveConnectionMonitor.java&quot;&gt;DbcpActiveConnectionMonitor.java&lt;/a&gt; : 여러 개의 Datasource의 Active connection 갯수를 구할 때 사용. getConnection에서 URL별로 active connection 갯수를 저장해 두었다가, Btrace에서 이벤트를 날리면 출력을 해주고 종료&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첨부한 스크립트 중 1,2번은 getConnection이 한번 호출되고 나면 스크립트를 종료하는 구조라서 큰 부하는 없겠지만, 증가 추이 등의 통계 정보를 쌓는기능 등이 필요하다면 Field poolField = field(DS_CLASS, &quot;connectionPool&quot;); 같은 부분도 중복호출되지 않게 하는 처리가 필요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소스는 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/btrace-scripts&quot;&gt;examples/btrace-scripts&lt;/a&gt;에 올렸습니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Kent Beck과의 인터뷰 중에서..</title>
      <link>https://blog.benelog.net//2815291.html</link>
      <pubDate>Tue, 21 Jun 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2815291.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2010년 9월에 Kent Beck과의 인터뷰가 SE-Radio 사이트에 올라왔었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.se-radio.net/2010/09/episode-167-the-history-of-junit-and-the-future-of-testing-with-kent-beck/&quot; class=&quot;bare&quot;&gt;http://www.se-radio.net/2010/09/episode-167-the-history-of-junit-and-the-future-of-testing-with-kent-beck/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 인터뷰 중에 아래 내용이 가장 인상 깊었고, 몇달이 지나서도 기억에 남아있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;19분 경에 나오는 내용입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;받아쓴_내용&quot;&gt;받아쓴 내용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Martin(사회자):&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Do you think you&amp;#8217;ve convinced most of people you talk to?&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Kent Beck :&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I don&amp;#8217;t try to convince people any more.I think I spent probably 10 years really trying to convince people TDD, pair programming, you should do this, you should do that and I don&amp;#8217;t do that any more.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I am always working to improve my own practice, I am eager to share what I learn and I try and listen and understand what other people learned and their experiences as well. So I don&amp;#8217;t like &amp;#8230;&amp;#8203; I don&amp;#8217;t really keep score by how many people do TDD.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If the practice of software development is improving overall, then I think that&amp;#8217;s a great thing, If I have some small influence on that, then I think that&amp;#8217;s a great thing too.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;번역&quot;&gt;번역&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;마틴(사회자)&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그동안 이야기한 사람들을 대부분 설득했다고 생각합니까?&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;켄트벡 :&lt;/div&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 더 이상 사람들을 설득하려 하지 않습니다. 저는 거의 10년동안 사람들에게 TDD를 꼭해라, 짝프로그래밍을 꼭 해라고 설득하려고 애써왔지만, 더 이상 그렇게 하지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전 항상 제가 수행하는 방식을 개선하려고 일하고, 제가 시도하고 배운 것을 공유하려고 노력합니다.  다른 사람이 배운 것과 경험도 듣고 이해하려 하구요. 그래서 전 얼마나 많은 사람들이 TDD를 하는지를 가지고 점수를 매기지는 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소프트웨어를 개발하는 방식이 전반적으로 나아진다면 전 그걸 대단한 일이라고 생각하고, 만약 제가 거기에 약간의 영향이라도 끼친다면 역시나 대단한 일이라고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring AOP의 Aspect에서 order 값에 따른 적용순서 확인해보기</title>
      <link>https://blog.benelog.net//2805903.html</link>
      <pubDate>Mon, 30 May 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2805903.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring AOP에서 동일한 target클래스에 결합되는 Aspect의 우선순위는  &lt;a href=&quot;http://static.springsource.org/spring/docs/3.0.x/api/org/springframework/core/Ordered.html&quot;&gt;Ordered&lt;/a&gt; 인터페이스를 구현하거나 @Order 애너테이션으로 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같은 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;

@Aspect
@Order(1)
public class OrderOneAspect {
    @Before(&quot;execution(void *.run())&quot;)
    public void printOrder(JoinPoint jp){
        System.out.println(&quot;order 1&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;



@Aspect
public class OrderTwoAspect implements Ordered{
    @Before(&quot;execution(void *.run())&quot;)
    public void printOrder(JoinPoint jp){
        System.out.println(&quot;order 2&quot;);
    }
    public int getOrder() {
        return 2;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OrderOneAspect 은 애너테이션으로, OrderTwoAspect은 인터페이스로 order 값을 선언했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://static.springsource.org/spring/docs/3.0.x/api/org/springframework/core/Ordered.html&quot;&gt;Ordered&lt;/a&gt; 인터페이스의  javadoc에 보면 order의 숫자 값이 작을수록 우선 순위가 높다고 적혀 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The actual order can be interpreted as prioritization, with the first object (with the lowest order value) having the highest priority.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다면 AOP에서는 여러개의 Aspect를 엮을 때 order값을 낮게 선언한 Aspect가 먼저 실행될까요?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한번 테스트코드를 짜봤습니다. 위의 OrderOneAspect , OrderTwoAspect와 함께, 각각 0과 -1의 order를 가지는 Aspect를 더 추가했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Aspect
@Order(0)
public class OrderZeroAspect {
    @Before(&quot;execution(void *.run())&quot;)
    public void printOrder(JoinPoint jp){
        System.out.println(&quot;order 0&quot;);
    }
}

@Aspect
@Order(-1)
public class OrderMinusOneAspect {
    @Before(&quot;execution(void *.run())&quot;)
    public void printOrder(JoinPoint jp){
        System.out.println(&quot;order -1&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Target클래스와 Proxy를 만들어줄 설정을 javaConfig를 이용해서 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class MainBeans {
    @Bean
    public ProxyConfig proxyCreator(){
        return new AspectJAwareAdvisorAutoProxyCreator();
    }
    @Bean
    public Runnable main(){
        return new Runnable(){
            public void run() {
                System.out.println(&quot;target executed&quot;);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aspect는 모두 &quot;before&quot; Advice로 그 Aspect의 order 값을 찍도록했고, 적용대상인 클래스는 단순히 &quot;target executed&quot;를 출력합니다. 출력창에 찍힌 내용을 보면 어떤 순서로 Aspect가 적용되는지 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모두 ApplicationContext에 등록해서 Target클래스를 출력하는 테스트 코드를 만들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class AspectOrderTest {
    @Test
    public void checkOrder() throws Exception {
        GenericApplicationContext  context =
            new AnnotationConfigApplicationContext(
                OrderZeroAspect.class,
                OrderMinusOneAspect.class,
                OrderTwoAspect.class,
                OrderOneAspect.class,
                MainBeans.class);
        Runnable printer = context.getBean(Runnable.class);
        printer.run();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;출력 결과는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;order 2
order 1
order 0
order -1
target executed&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Order의 숫자가 큰 값이 먼저 나옵니다. 큰 Order 값인, 낮은 우선 순위인 Aspect가 먼저 실행되었다는 결과입니다. 제가 처음에 매뉴얼만 보고 이해한 의미와는 반대의 결과였습니다. 제가 설정을 잘못한 부분이 있는지도 모르겠지만, 이 결과만을 해석을 해보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Aspect에서 우선순위가 높다는 의미는 &apos;안쪽의 proxy가 된다.&apos;는 의미입니다. 즉, Proxy를 만들 때 Order값이 작은, 우선 순위가 높은 Aspect부터 먼저 Proxy를 만들고, 그 뒤에 다음 우선순위의 proxy들을 차례로 입혀갑니다. 그러니 &apos;before&apos; 나 &apos;Around&amp;#8217;Advice라면 가장 바깥 쪽에 Proxy로 입혀진 , order값이 큰, 우선순위가 낮은 Aspect의 코드가 먼저 실행됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하면, Order값 숫자가 작을수록 우선 순위가 높아지고, 우선 적용되어서 더 안 쪽에서 감싸지는 proxy가 되어서  타겟 Object의 코드와 붙어있는 Apspect가 된다는 의미입니다.  즉, Aspect 설정에서 order값의 숫자가 클 수록 target객체에서 더 멀어지는 바깥 쪽의 Proxy로 결합됩니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring의 @Scheduled로 지정된 크론 표현식을 테스트하기</title>
      <link>https://blog.benelog.net//2802946.html</link>
      <pubDate>Mon, 23 May 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2802946.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;변경 이력&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2024.06.05 스프링 프레임워크 버전 5.3 이상에 맞는 코드로 수정&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링에서는 &lt;code&gt;@Schedule&lt;/code&gt; 애너테이션을 통해 크론 표현식으로 특정 메서드를 예약 실행할 수 있습니다.
그런데 실제로 잘 실행될지는 해당 시간이 되어야 알 수 있으므로 실수가 있어도 늦게 발견될 가능성이 높습니다.
예를 들면 매일 5시 20분, 40분, 60분에 실행될 일정을 지정하고자 했는데 &quot;* 0/20 5 * * * ?&quot;로 써야 할 표현식을 &quot;* 0,20 5 * * * ?&quot;으로 써놓고는 실운영 서버에 배포 한 후 하루가 지난 다음에 실수를 발견하는 경우입니다.
크론 표현식이 기대대로 해석되는지도 이 글을 참고하여 테스트 코드로 검증을 한다면 이런 사고를 막을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예제는 스프링 프레임워크 6.1.8 버전에 의존하여 작성되었으나 버전 5.3 이상에서는 동일한 방식으로 사용할 수 있을 것으로 예상합니다.
5.3버전을 기준으로 날짜 관련 클래스가 JDK 8의 API를 활용하는 방식으로 바뀌었습니다.
스프링 3.0 에서는 &lt;code&gt;java.util.Date&lt;/code&gt; 등의 과거 API를 활용하는 방식으로 응용하실 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;크론_표현식으로_예약_실행할_메서드_지정&quot;&gt;크론 표현식으로 예약 실행할 메서드 지정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대상이 되는 메서드에 &lt;code&gt;@Schedule&lt;/code&gt; 애너테이션의 &lt;code&gt;cron&lt;/code&gt;, &lt;code&gt;zone&lt;/code&gt; 속성을 지정합니다.
&lt;code&gt;zone&lt;/code&gt; 속성은 필수는 아니지만 지정하면 명확성이 높아지고 시스템 기본값에 영향받지 않는다는 장점이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;JobSchedule.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.time.Instant;
import java.util.Properties;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@EnableScheduling
public class JobSchedule {

  @Scheduled(cron = &quot;0 0,10 0 * * ?&quot;, zone = &quot;Asia/Seoul&quot;)
  public void startHelloJob() {
    // 실행할 코드
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;테스트_코드&quot;&gt;테스트 코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;크론 표현식을 추출하고 검사하는 코드는 ScheduleTestUtils 클래스로 분리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import org.junit.jupiter.api.Test;

class JobScheduleTest {
  @Test
  void scheduleHelloJob() {
    var initialTime = LocalDateTime.of(2024, 6, 10, 0, 5);
    List&amp;lt;LocalDateTime&amp;gt; expectedTimes = List.of(
        LocalDateTime.of(2024, 6, 10, 0, 10),
        LocalDateTime.of(2024, 6, 11, 0, 0),
        LocalDateTime.of(2024, 6, 11, 0, 10)
    );
    ScheduleTestUtils.assertCronExpression(
        JobSchedule.class, &quot;startHelloJob&quot;,
        toInstant(initialTime),
        expectedTimes.stream().map(this::toInstant).toList()
    );
  }

  private Instant toInstant(LocalDateTime time) {
    return time.atZone(ZoneId.of(&quot;Asia/Seoul&quot;)).toInstant();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;ScheduleTestUtils.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import static org.assertj.core.api.Assertions.assertThat;

import java.lang.reflect.Method;
import java.time.Instant;
import java.util.List;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.SimpleTriggerContext;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

public class ScheduleTestUtils {
  public static void assertCronExpression(
      Class&amp;lt;?&amp;gt; targetClass, String methodName,
      Instant initialTime, List&amp;lt;Instant&amp;gt; expectedTimes
  ) {
    Method method = ReflectionUtils.findMethod(targetClass, methodName);
    assertThat(method).isNotNull();

    Scheduled scheduled = method.getAnnotation(Scheduled.class);
    CronTrigger trigger = getTrigger(scheduled);
    var context = new SimpleTriggerContext(initialTime, initialTime, initialTime);

    for (Instant expected : expectedTimes) {
      Instant actual = trigger.nextExecution(context);
      assertThat(actual).isEqualTo(expected);
      context.update(actual, actual, actual);
    }
  }

  private static CronTrigger getTrigger(Scheduled scheduled) {
    // 스프링의 ScheduledAnnotationBeanPostProcessor 코드를 참고함
    if (StringUtils.hasText(scheduled.zone())) {
      return new CronTrigger(scheduled.cron(), StringUtils.parseTimeZoneString(scheduled.zone()));
    } else {
      return new CronTrigger(scheduled.cron());
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Quartz 설정을 읽어서 CronExpression 테스트</title>
      <link>https://blog.benelog.net//2802943.html</link>
      <pubDate>Mon, 23 May 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2802943.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cron expression은 실수하기가 쉽고, 오류가 뒤늦게서야 발견됩니다. 매일 5시 20분, 40분,60분에 실행될 일정을 지정하고자 했는데 &quot;* 0/20 5 * * * ?&quot;로 써야 할 표현식을  &quot;* 0,20 5 * * * ?&quot;으로 써놓고는 실운영 서버에 배포해서 하루가 지난 다음에 실행결과를 보고서야 실수를 발견한 경험을 해보신 분들이 많으실 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Cron expression도 테스트 코드를 짜서 검증을 해본다면 치명적인 실수를 막을 기회가 더 많아집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 예제는  ApplicationContext에 설정되어 있는 Quartz의 CronTrigger의 일정을 테스트하는 코드입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;applicationcontext의_스케줄링_설정_파일&quot;&gt;ApplicationContext의 스케줄링 설정 파일&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;bean id=&quot;schedulerFactoryBean&quot; class=&quot;org.springframework.scheduling.quartz.SchedulerFactoryBean&quot;&amp;gt;
     &amp;lt;property name=&quot;triggers&quot; ref=&quot;triggers&quot;/&amp;gt;
&amp;lt;/bean&amp;gt;
&amp;lt;util:list id=&quot;triggers&quot;&amp;gt;
     &amp;lt;bean p:jobName=&quot;baseballJob&quot; p:cronE-pression=&quot;0 * * * * ?&quot; parent=&quot;jobTrigger&quot;/&amp;gt;
     &amp;lt;bean p:jobName=&quot;baseballExportJob&quot; p:cronE-pression=&quot;30 5-7 * * * ?&quot; parent=&quot;jobTrigger&quot;/&amp;gt;
&amp;lt;/util:list&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 설정 예제에서는 반복되는 설정을 간편하게 해주는 FactoryBean을 썼습니다. parent=&quot;jobTrigger&quot;라고 지정된 부분이 FactoryBean 클래스와 연결되는 bean id를 지정한 속성입니다.
여기에 쓰인 FactoryBean은 이&quot;jobName&quot;과 &quot;cronExpression&quot; 속성을 받아서 org.quartz.CronTrigger 타입의 bean을 생성해주는 역할을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;JobTriggerFactoryBean&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package edu.batch.support.launch;

import java.util.HashMap;
import java.util.Map;

import org.quartz.Trigger;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.sample.quartz.JobLauncherDetails;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.scheduling.quartz.CronTriggerBean;
import org.springframework.scheduling.quartz.JobDetailBean;
import org.springframework.util.Assert;

/**
 * @author sanghyuk.jung
 */
public class JobTriggerFactoryBean implements FactoryBean&amp;lt;Trigger&amp;gt;, InitializingBean {
    private JobLocator jobLocator;
    private JobLauncher jobLauncher;
    private String jobName;
    private String cronExpression;
    private String triggerName;

    /**
     * @return
     * @throws Exception
     * @see org.springframework.beans.factory.FactoryBean#getObject()
     */
    public Trigger getObject() throws Exception {
        CronTriggerBean trigger = new CronTriggerBean();
        trigger.setCronExpression(cronExpression);
        JobDetailBean jobDetail = createJobDetail();
        trigger.setJobDetail(jobDetail);
        if(triggerName == null ){
        trigger.setName(jobName+&quot;Trigger&quot;);
        } else {
            trigger.setName(triggerName);
        }
        trigger.afterPropertiesSet();
        return trigger;
    }

    private JobDetailBean createJobDetail() {
        JobDetailBean jobDetail = new JobDetailBean();
        jobDetail.setName(jobName);
        Map&amp;lt;String, Object&amp;gt; jobData = new HashMap&amp;lt;String, Object&amp;gt;();
        jobData.put(&quot;jobName&quot;, jobName);
        jobData.put(&quot;jobLocator&quot;, jobLocator);
        jobData.put(&quot;jobLauncher&quot;, jobLauncher);
        jobDetail.setJobDataAsMap(jobData);
        jobDetail.setJobClass(JobLauncherDetails.class);
        jobDetail.afterPropertiesSet();
        return jobDetail;
    }

    /**
     * @return
     * @see org.springframework.beans.factory.FactoryBean#getObjectType()
     */
    public Class&amp;lt;Trigger&amp;gt; getObjectType() {
        return Trigger.class;
    }

    /**
     * @return
     * @see org.springframework.beans.factory.FactoryBean#isSingleton()
     */
    public boolean isSingleton() {
        return false;
    }

    public void setJobLocator(JobLocator jobLocator) {
        this.jobLocator = jobLocator;
    }

    public void setJobLauncher(JobLauncher jobLauncher) {
        this.jobLauncher = jobLauncher;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public void setCronExpression(String cronExpression) {
        this.cronExpression = cronExpression;
    }

    /**
     * @throws Exception
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(jobName, &quot;jobName must be provided&quot;);
        Assert.notNull(jobLocator, &quot;jobLocator name must be provided&quot;);
        Assert.notNull(jobLauncher, &quot;jobLauncher name must be provided&quot;);
    }

    public void setTriggerName(String triggerName) {
        this.triggerName = triggerName;

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;테스트_코드&quot;&gt;테스트 코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;triggers라는 bean 이름으로 List&amp;lt;CronTrigger&amp;gt; type의 객체를 가지고 와서, List안에서 지정된 trigger의 이름을 탐색한다음에 그 안에서 그 trigger의 cron e-pression을 검사했습니다.  테스트 코드를 간결하게 유지하기 위해서 Cron e-pression을 검사하는 코드는  QuartzCronExpressionTestUtils라는 클래스로 분리해서 static import로 처리했습니다.
QuartzCronExpressionTestUtils.findTriggerByName 메서드는 List&amp;lt;CronTrigger&amp;gt; 타입의 객체가 담고 있는 여러개의 CronTrigger에서 지정된 이름의 CronTrigger를 반환해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package edu.batch.baseball.schedule;

import static edu.batch.support.launch.QuartzCronE-pressionTestUtils.*;

import java.text.ParseException;
import java.util.Arrays;
import java.util.List;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.quartz.CronTrigger;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( { &quot;classpath:/launch-context.xml&quot; })
public class BaseballQuartzScheduleTest {

    private static final String DATE_PATTERN = &quot;yyyy/MM/dd hh:mm:ss&quot;;

    @Resource(name = &quot;triggers&quot;)
    List&amp;lt;CronTrigger&amp;gt; triggers;

    @Test
    public void testbaseballJobTriggerSchedule() throws ParseException {
        CronTrigger trigger = findTriggerByName(triggers, &quot;baseballJobTrigger&quot;);
        String initialTime = &quot;2010/09/01 09:00:00&quot;;
        List&amp;lt;String&amp;gt; expectedTimeList = Arrays.asList(
                &quot;2010/09/01 09:01:00&quot;,
                &quot;2010/09/01 09:02:00&quot;,
                &quot;2010/09/01 09:03:00&quot;,
                &quot;2010/09/01 09:04:00&quot;);
        assertSchedule(trigger, initialTime, expectedTimeList, DATE_PATTERN);
    }

    @Test
    public void testbaseballExportJobTriggerSchedule() throws ParseException {
        CronTrigger trigger = findTriggerByName(triggers, &quot;baseballExportJobTrigger&quot;);
        String initialTime = &quot;2010/09/01 09:00:00&quot;;
        List&amp;lt;String&amp;gt; expectedTimeList = Arrays.asList(
                &quot;2010/09/01 09:05:30&quot;,
                &quot;2010/09/01 09:06:30&quot;,
                &quot;2010/09/01 09:07:30&quot;,
                &quot;2010/09/01 10:05:30&quot;,
                &quot;2010/09/01 10:06:30&quot;,
                &quot;2010/09/01 10:07:30&quot;);
        assertSchedule(trigger, initialTime, expectedTimeList, DATE_PATTERN);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;QuartzCronExpressionTestUtils&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package edu.batch.support.launch;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.text.ParseException;
import java.util.Date;
import java.util.List;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.DateUtils;
import org.quartz.CronTrigger;

public class QuartzCronExpressionTestUtils {

    public static void assertSchedule(CronTrigger trigger, String initialTime,
            List&amp;lt;String&amp;gt; expectedTimeList, String datePattern) throws ParseException {
            Date previousStartTime = DateUtils.parseDate(initialTime,   new String[]{datePattern});

            for(String expectedTime : expectedTimeList){
                trigger.setStartTime(previousStartTime);
                Date nextExecutionTime =  trigger.getFireTimeAfter(previousStartTime);
                String actualTime = DateFormatUtils.format(nextExecutionTime, datePattern);
                assertThat(&quot;executed on expected time&quot;, actualTime, is(expectedTime));
                previousStartTime = nextExecutionTime;
            }
        }

    public static CronTrigger findTriggerByName(List&amp;lt;CronTrigger&amp;gt; triggers, String triggerName) {
        for (CronTrigger trigger : triggers) {
            if (triggerName.equals(trigger.getName())) {
                return trigger;
            }
        }
        throw new IllegalArgumentException(&quot;cannot find trigger : &quot;
                + triggerName);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>SpringSource Tool Suite의 Java Agent based reloading 사용해 보기</title>
      <link>https://blog.benelog.net//2796964.html</link>
      <pubDate>Mon, 9 May 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2796964.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SpringSource Tool Suite의 Java Agent based reloading은 개발할 때  .class파일을 고치면 그 파일만 리로딩을 시켜주는 기능입니다. JRebel과도 유사한데 아직 &quot;Experimental&apos; 표시가 붙어있고, 생긴지가 얼마안되는 기능이라서 JRebel만큼 성숙한 기술인지는 잘 모르겠습니다. 그래도 모든 경우에 다 reloading이 되지 않더라도 한번이라도 서버내렸다 올리는 시간을 절약할 수 있으면 그만큼 이득이기 때문에 없는 것보다는 도움이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 도구들이 있으면 좋기는 하지만  WAS올리기 전에 테스트 코드로 왠만한 건 다 검증을 하고, WAS 올려서는 JSP같은 View만 고치는 개발 방식이 바람직합니다.  테스트 코드를 잘 짜고 있다면 &apos;개발 중에 WAS 리로딩한다고 시간이 많이 들어요..&apos; 와 같은 이야기가 별로 안 나올 것입니다. 테스트 코드 작성은 익숙해지면 시간이 별로 안 걸리는 일입니다. 그리고 테스트 코드를 만들면 로직이 있는 모듈의 가까운 위치에서 다양한 케이스를 반복해서 테스트해 볼 수 있고, 에러 추적과 디버깅이 훨씬 편해집니다. 아무리 리로딩이 잘 지원된다고 해도, 화면을 띄어서 손으로 데이터를 매번 입력하는 시간을 없애줄 수는 없고, 리로딩이 시간보다 훨씬 긴  에러 추적과 디버깅 시간을 줄여줄 수는 없습니다. 그래서 결국에는 테스트코드 작성하는 것이 개발시간을 더 빠르게 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;STS를 설치할 때 SpringSource Tool Suite를 한번에 받아서 설치했다면 Tc server도 같이 설치되지만, 이미 깔려진 Eclipse 위에 update를 했다면 tc Server는 아래 URL에서 별도로 받아야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://s9.springsource.com/products/tc-server-developer-edition-preview&quot; class=&quot;bare&quot;&gt;http://s9.springsource.com/products/tc-server-developer-edition-preview&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;java_agent_based_reloading_설정&quot;&gt;Java agent based Reloading 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&apos;Servers&apos; Tab에서 New메뉴로 새 서버를 추가한다.
&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/1_new-server.jpg&quot; alt=&quot;1_new-server.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tc Server를 선택한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;SpringSource tc Server v2.0 혹은 v2.1 또는 v2.5를 선택한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;참고로 SpringSource Tool Suite 2.6.1부터 tc server v2.5가 포함되어 있고, &quot;VMware vFabric tc Server v2.5&quot;라는 이름으로 &quot;VM Ware&quot; 분류 폴더 아래에 포함되어 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/20_tc-server-select.jpg&quot; alt=&quot;20_tc-server-select.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;처음 설정하는 것이라면, Tc sever가 설치된 위치를 지정합니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/21_tc-server-select.jpg&quot; alt=&quot;21_tc-server-select.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;처음 설정하는 것이라면 &quot;Create new instance&quot;를 선택한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/4_new-server.jpg&quot; alt=&quot;4_new-server.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;같이 설치할 모듈을 지정합니다. 간단하게 &apos;base&amp;#8217;와 &apos;nio&amp;#8217;만 선택해 된다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/5_new-server.jpg&quot; alt=&quot;5_new-server.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;설정된 Server의 &quot;Overview&quot; 탭에서 &quot;Enable Java Agent-based reloading&quot;을 선택한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/6_servers.jpg&quot; alt=&quot;6_servers.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;시험 삼아서 서버를 시작해 본다..&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/7_servers-start.jpg&quot; alt=&quot;7_servers-start&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;서버가 시작되면서 &quot;Agent based reloading is active&quot;라는 메시지가 처음에 뜨면 제대로 설정이 된 것이다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/8_start-log.jpg&quot; alt=&quot;8_start-log.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;java_agent_based_reloading_테스트_해보기&quot;&gt;Java agent based reloading 테스트 해보기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트용 프로젝트를 생성해서 reloading이 되는지 확인하는 과정이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;Ctrl +N을 누르고 &quot;Spring Template Project&quot;를 선택한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/9_new-spring-template.jpg&quot; alt=&quot;9_new-spring-template.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&quot;Spring MVC project&quot;를 선택한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/10_new-spring-mvc-project.jpg&quot; alt=&quot;10_new-spring-mvc-project.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로젝트명과 상위 패키지이름을 적는다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/11_new-spring-mvc-project.jpg&quot; alt=&quot;11_new-spring-mvc-project.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;생성된 프로젝트를 Run AS&amp;#8594; Run On Server로 실행한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/12_run-on-the-server.jpg&quot; alt=&quot;12_run-on-the-server.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;실행할 서버는 Java Agent Based Reloading을 설정한 tc Server로 지정한다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/13_run-on-the-server.jpg&quot; alt=&quot;13_run-on-the-server.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;서버가 올라간 다음에 템플릿의 HomeController 클래스를 수정해보고, 메서드의 내용을 수정한 다음에  아래와 같이 전체 &quot;Realoding&amp;#8230;&amp;#8203;&quot; 메시지가 보이며 applicationContext loading 없이 해당 클래스만 리로딩 되는 것을 확인한다.
&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sts-reload/14_modify-controller.jpg&quot; alt=&quot;14_modify-controller&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;테스트_결과&quot;&gt;테스트 결과&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Dynamic web project가 아니고 external web module로 추가한 경우 - 잘 됨&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Servlet만으로 된 프로젝트 - 잘됨&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Application Context 파일 수정 - 되기도 하고 안 되기도 함&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring MVC의 controller에 @RequestMapping이 달린 새로운 메서드를 추가한 것을 인식 - 잘 안 됨&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>java.net.InetAddress + SpEL로 서버이름을 Bean 설정에서 바로 얻어오기</title>
      <link>https://blog.benelog.net//2795888.html</link>
      <pubDate>Fri, 6 May 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2795888.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring 3.0부터 추가된 SpEL(Spring E-pression Language)를 응용한 사례입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;application context 파일 선언만으로 특정 빈의 속성에 서버 이름을 넣으려면 어떻게 해야하냐는 질문을 받았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;질문자는 아래의 bean설정에 &quot;serverName&quot;이라고 지정된 곳에 각각 실행되는 서버마다 다른 이름을 넣고 싶어했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;persistentMessageStore 선언&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;bean id=&quot;persistentMessageStore&quot; class=&quot;org.springframework.integration.jdbc.JdbcMessageStore&quot;&amp;gt;
   &amp;lt;property name=&quot;region&quot; value=&quot;serverName&quot; /&amp;gt;
....

&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;나름대로의 FactoryBean을 따로 만들거나 JavaConfig을 써도 쉽게 풀리는 문제이지만, SpEL을 활용하는 것이 기존 설정을 가장 적게 바꾸는 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 같이 java.net.InetAddress.getLocalHost를 bean으로 등록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;bean id=&quot;localHost&quot; class=&quot;java.net.InetAddress&quot; factory-method=&quot;getLocalHost&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SPel이 잘 먹는지는 다음과 같이 테스트 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class HostNameTest {
    @Value(&quot;#{localHost.hostName}&quot;) String hostName;
    @Test
    public void hostNameShouldBePrinted() throws Exception {
        String expectedHostName = InetAddress.getLocalHost().getHostName();
        assertThat(hostName,is(expectedHostName));
        System.out.println(hostName);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 테스트 코드에서는 @ContextConfiuguration선언 뒤에 Application context 파일 위치를 지정하지 않았으므로,  default로 참조되는 같은 패키지 디렉터리의 HostNameTest-context.xml에 &apos;localHost&apos; bean이 선언되어 있어야 겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제일 처음의 코드1의 bean선언에서도 마찬가지로 &lt;code&gt;#{localHost.hostName}&lt;/code&gt; 을 참조할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;bean id=&quot;persistentMessageStore&quot; class=&quot;org.springframework.integration.jdbc.JdbcMessageStore&quot;&amp;gt;
   &amp;lt;property name=&quot;region&quot; value=&quot;#{localHost.hostName}&quot; /&amp;gt;
....

&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SpEL을 활용하면 아래와 같이 bean사이의 연결을 더 유연하게 할 수도 있지만, 컴파일타임에 검증되지 않는다는 단점이 있습니다. 이를 보완하려면 오타를 검증할 수 있는 테스트 코드가 있어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다른 방법으로는,  JavaConfig를 활용해서 직접 명시적으로 메서드를 호출해서 hostName을 주입해도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Bean JdbcMessageStore persistentMessageStore(){
    JdbcMessageStore store = new JdbcMessageStore ();
    store.setRegion(localHost.getHostName());
    return store;
}

@Bean public InetAddress localHost() throws UnknownHostException {
    return  InetAddress.getLocalHost();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>파일 업로드를 하는 Servlet을 MockHttpServletRequest로 테스트하기</title>
      <link>https://blog.benelog.net//2790336.html</link>
      <pubDate>Sun, 24 Apr 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2790336.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;파일 update를 처리하는 Servlet도 Spring의 MockHttpSerlvetRequest와 MockHttpServletResponse로 테스트 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음 링크에 있는 소스를 참고했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://milandinic.blogspot.com/2009/05/testing-upload-servlet-under-spring.html&quot; class=&quot;bare&quot;&gt;http://milandinic.blogspot.com/2009/05/testing-upload-servlet-under-spring.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;request의 content 속성에 파일내용 등을 포함시켜 주면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 예제에서 Assert부분은 화면에 출력되는 문자열을 검사하는 방식입니다.
테스트 하고자하는 목적에 따라 파일업로드가 되었을 때의 특정 위치에 파일이 생성된 것을 확인한다거나, 그 뒤에 호출되는 클래스를 행위검증하는 방식등을 다양하게 응용하실 수 있을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;publicclassUploadServletTest {
    UploadServlet servlet =newUploadServlet() ;
    MockHttpServletRequest request =newMockHttpServletRequest();
    MockHttpServletResponse response =newMockHttpServletResponse();

    private static final String ENDLINE =&quot;\r\n&quot;;

    private static final String BOUNDARY =&quot;qWeRtY&quot;;

   @Test
    public void testUploadNormalFile() throws Exception {
        // given
        String fileContent =&quot;for upload test&quot;;
        String fileName =&quot;message.txt&quot;;
        String reqContent = createContentWithFile(fileName, fileContent);
        request.setContent(reqContent.getBytes());
        request.setContentType(&quot;multipart/form-data; boundary=&quot;+ BOUNDARY);
        request.setMethod(&quot;POST&quot;);

        // when
        servlet.service(request, response);

       // then
       String output = response.getContentAsString();
      assertTrue(&quot;정상적으로 업로드 되었을 때에는&quot;, output.contains(&quot;File size&quot;));
    }

    @Test
   public void testUploadEmptyFile() throws Exception {
       // given
       String fileContent =&quot;&quot;;
       String fileName =&quot;message.txt&quot;;
       String reqContent = createContentWithFile(fileName, fileContent);
       request.setContentType(&quot;multipart/form-data; boundary=&quot;+ BOUNDARY);
       request.setMethod(&quot;POST&quot;);
       request.setContent(reqContent.getBytes());

       // when
       servlet.service(request, response);

      // then
      String output = response.getContentAsString();
      assertThat(&quot;빈 파일이 올라갔을 때에는&quot;, output, is(&quot;No binary data contains&quot;));
    }

    private String createContentWithFile(String fileName, String fileContent) {
        StringBuilder reqContent =newStringBuilder();
        reqContent.append(&quot;--&quot;+ BOUNDARY + ENDLINE);
        reqContent.append(&quot;Content-Disposition: form-data; name=\&quot;myfile\&quot;;&quot;
        +&quot; filename=\&quot;&quot;+ fileName +&quot;\&quot;&quot;+ ENDLINE);
        reqContent.append(ENDLINE);
        reqContent.append(fileContent);
        reqContent.append(ENDLINE);
        reqContent.append(&quot;--&quot;+ BOUNDARY +&quot;--&quot;+ ENDLINE);
        return reqContent.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 Spring에서는 MockMultipartHttpServletRequest와 MockMultipartFile 같은 첨부파일에 특화된 테스트 전용 클래스를 제공하기는 하지만, Spring MVC를 사용하지 않는 그냥 Servlet에서는 위의 방식처럼 MockHttpServletRequest을 사용해야 합니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>내가 생각하는 TDD</title>
      <link>https://blog.benelog.net//2766714.html</link>
      <pubDate>Sun, 6 Mar 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2766714.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://cdn.mystorypie.com/entities/image/invention/694c7f84b711aa600a8b3cc4/hero_2025-12-25_00-06-56-013.webp&quot; alt=&quot;image&quot; width=&quot;332&quot; height=&quot;500&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이미지 출처: &lt;a href=&quot;https://mystorypie.com/learn/ko/invention/traffic-light/&quot; class=&quot;bare&quot;&gt;https://mystorypie.com/learn/ko/invention/traffic-light/&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컴퓨터가 내 의도를 이해했는지 표시하는 신호등을 설치하고, 그 신호에 의지해서 사람이 더 읽기 편하고 고치기 쉬운 코드를 개발하는 기법입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에는 내가 만드는 프로그램 조각이 어떤 상황에서 어떤 결과를 내는지 설명하는 글을 씁니다. 이것을 명세나 테스트라고 부르고 컴퓨터가 해석하고 실행가능한 형식으로 만듭니다. 그 설명을 컴퓨터가 실행해서 의도한 결과인지를 신호등으로 알려주게 합니다. 다음에는 그 설명과 맞아떨어지는 코드를 작성합니다. 신호등은 파란색이 되었다면 코드에 실린 사람의 의도를 모두 컴퓨터가 잘 받아들였다는 의미입니다. 파란색은 다음 발걸음을 내딛을 수 있다는 신호이기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컴퓨터를 이해시킨 다음에는 사람이 더 편안하게 읽을 수 있는 코드로 다듬어야 합니다. 다시 코드를 보는 사람이 헷갈리지 않도록 중복을 없애고, 의도를 더 확실하게 표현하도록 이름을 바꾸거나 프로그램 조각의 일부를 빼내서 정리합니다. 자신이 있어서 처음부터 컴퓨터와 사람에게 모두 충분한 코드를 만들었다면, 발걸음을 크게 해서 바로 그 다음 설명과 신호등을 만듭니다. 교통 신호등처럼 시간이 고정되어 있지 않고, 신호 사이의 간격, 신호등이 확인하는 설명의 크기와 다음 신호등을 만드는 시기를 마음대로 조절해도 됩니다. 그런데 하다보면 자주 파란불을 보고 싶어서 간격을 짧게 만들고 싶어집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 과정을 거치다보면 여러 가지를 얻습니다. 신호등이 연결된 설명서는 컴퓨터와 사람이 모두 이해할 수 있는 문서입니다. 잘 설명되고, 검증될 수 있는 코드들은 역할과 책임이 명확해서 다음에 기능을 추가하거나, 수정을 할 때 더 적은 노력이 들어갑니다. 중간 중간 내부에 설치된 신호등을 한꺼번에 켜보면 엑스레이 사진처럼 프로그램 속이 들여다 보입니다. 깊숙이 박혀 있는 오류를 찾아내는 시간도 줄여줍니다. 무엇보다 빨간불, 파란불을 왔다갔다 하다보면 프로그래밍이 더 역동적인 일이 됩니다. 컴퓨터와 반응을 주고 받는 것이 마치 게임과 비슷해집니다. 파란불을 볼 때마다 뭔가 끝을 내고 성취했다고 느껴지고 칭찬을 받는 기분으로 그 다음 작업을 할 수 있는 힘을 얻습니다. 그래서 프로그래밍이 더 재미있어지고, 집중도 잘 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;신입사원 교육을 하면서 주로 유명인들이 이야기한 TDD의 여러 측면들을 인용해서 설명을 했습니다. 그런데 정말 저만의 단어로 TDD를 설명한다면 어떻게 할까. 스스로 궁금해졌습니다. 적고 나니 계속 다듬어야 할 것 같네요.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>스프링과 클라우드가 무슨 상관이 있을까?</title>
      <link>https://blog.benelog.net//2765024.html</link>
      <pubDate>Thu, 3 Mar 2011 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2765024.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(덧붙임: 이글을 쓰던 시점의 CloudFoundry는 지금의 CloudFoundry와는 다른 기술이였고,  이전 CloudFoundry는 &lt;a href=&quot;http://classic.cloudfoundry.com/&quot; class=&quot;bare&quot;&gt;http://classic.cloudfoundry.com/&lt;/a&gt; 으로 옮기어 갔습니다. )&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스프링_클라우드를_내세우다&quot;&gt;스프링, 클라우드를 내세우다.&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링 프레임워크의 개발사인 스프링소스(SpringSource)와 VMWare는 최근 스프링이 클라우드 환경에 적합한 기술이라고 홍보를 하고 있다.
현재 스프링과 관련이 있는 클라우드 플랫폼은 아래와 같다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;vfabric&quot;&gt;vFabric&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;VMWare에서 제공하는 프라이빗 클라우드를 위한 솔루션이다. VMWare의 가상화 솔루션과 스프링소스의 미들웨어, 프레임웍, 개발도구 등을 합한 기술 스택을 제공한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;coludfoundry_httpwww_cloudfoundry_com&quot;&gt;Coludfoundry (&lt;a href=&quot;http://www.cloudfoundry.com/&quot; class=&quot;bare&quot;&gt;http://www.cloudfoundry.com/&lt;/a&gt; )&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링소스가 인수해서 운영하는 클라우드 서비스이다.
아마존의 AWS 인프라를 이용하고, Tomcat 등의 미들웨어를 제공한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;vmforce_httpwww_vmforce_com&quot;&gt;VMForce (&lt;a href=&quot;http://www.vmforce.com/&quot; class=&quot;bare&quot;&gt;http://www.vmforce.com/&lt;/a&gt; )&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;VMWare와 세일즈포스닷컴이 제휴한 클라우드이다. force.com 데이터베이스를 저장소를 이용하고, 스프링,Tomcat, vSphere 같은 VMWare의 솔루션이 들어간다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/spring-cloud/force-dot-com.png&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;VMFoce 클라우드 (출처 vmforce.com)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;google_app_engine&quot;&gt;Google App Engine&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구글과 VMWare의 제휴로 Google App Engine에 대한 지원이 스프링소스의 개발도구에 들어가게 되었다.
Spring-roo에서 Google App Engine의 저장소를 쓸 수 있도록 JPA의 Provider로  Datanucleus를 선택할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2010년 10월에 열린 스프링원 컨퍼런스에서도 클라우드에 대한 홍보는 두드러졌다.
자사의 클라우드 솔루션인 vFabric과 CloudFoundry는 물론, VMForce와 Google App Engine 과도 스프링을 묶어서 홍보했다.
스프링원 2010의 플레티엄 스폰서 세 곳 중 두 곳이 구글과 세일즈포스닷컴이였다는 것을 봐도 그 컨퍼런스의 초점은 알 수 있었다&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링은 애플리케이션 프레임워크일 뿐인데, 과연 클라우드와 무슨 관련이 있다는 것일까?
클라우드의 핵심기술은 가상화나 분산 저장소 같은 기술이 아닐까?
클라우드 환경에서 JVM만 제대로 갖추어진다면 스프링이 아닌 스트럿츠나 구글쥬스라고 해도 안 돌아갈 이유는 없지 않은가?
요즘은 서버 여러 대만 쓰는 기술이면 다 클라우드라는 이름을 붙이고 싶어하던데, 스프링도 이런 유행에 묻어 가고 싶은 마음이 아닐까?
여러 가지 의구심이 들만도 하다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나, 이를 억지 홍보 전략이라고만 생각하면서 그냥 넘어가는 것보다는 왜 VMWare ,구글, 세일즈포스닷컴이 스프링을 클라우드에 끼워 맞추면서 제휴를 하고 있는지를 분석해 본다면 기술의 흐름을 이해하는데 도움이 될 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;지난_10년_그리고_수익_모델&quot;&gt;지난 10년, 그리고 수익 모델&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;널리 알려 진 것처럼, 스프링은 EJB의 초기 시대에 Java EE 표준에 대한 대안 기술로 시작되었다.
스프링의 아버지 로드존슨은 초기 EJB의 어려움을 해결한 자신만의 프레임워크를 만들었고, 그 코드를 저서 “Expert one to one J2EE development”에 공개했다.
그 코드가 많은 개발자들의 호응을 얻어서 오픈소스 프로젝트가 되었다.
스프링 프레임워크는 세계 10대 은행 중 9개가 쓰고 있다고 할 정도로 인기를 끌었고[주1], 핵심 개발자들은 사업체를 차리고, 벤처 캐피탈의 투자를 받기에 이른다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링소스는 창업 이후에 계속 수익 모델을 고심 했을 것이다.
과연 애플리케이션 프레임워크만으로 어떻게 수익을 창출할 것인가?
한 때 스프링소스에서는 엔터프라이즈 서비스라는 것을 만들어서 라이선스를 산 고객에게 좀 더 편하게 마이너패치 버전을 제공하는 정책을 고려하기도 했었다[주2].&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 인수 합병을 통해 프레임워크 이외의 제품군도 확보를 한다.
스프링소스가 처음으로 인수한 업체는 Tomcat과 Apache Httpd의 핵심개발자들이 있는 Covalent라는 업체였다[주3].
프레임워크와 미들웨어를 통합한 서비스를 제공한다는 명분이였지만, 프레임워크만으로는 수익 창출에 한계가 있었기에 그런 결정을 했을 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;더욱 놀랍게도 스프링소스 자체가 2009년 4억 2천만 불(당시 환율로 5천3백억원 정도[주4]) 의 가격에 VMWare에 인수된다.
 00만원짜리 서버 53만대의 가격이니 그 가격도 놀랍지만 무엇보다 인수자가 의외였다.
 오라클이나 IBM처럼 자바 개발자가 친숙한 미들웨어를 많이 파는 업체가 아니였고, 자바와는 멀리 떨어진 듯한 가상화 솔루션으로 유명한 업체인 VMWare였기 때문이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로드존슨은 확장성 있는 사업모델을 고민했다[주5]. 컨설팅과 교육만으로는 인건비를 바탕으로 하는 수익 구조가 된다.
그래서 제품과 서비스를 판매하는 사업 모델도 필요했다.
그런데 제품도 상용 버전과 오픈소스 버전의 차이를 지나치게 크게 둔다면 오픈소스 커뮤니티를 홀대한다는 비판을 받을 수도 있다.
스프링소스에서는 Tc server라는 Tomcat에 기능을 강화한 제품을 판매하고 있다.
그러나 Tc Server에는 없는 버그가 Tomcat에는 있다면 많은 비판이 쏟아질 것이다.
제품 판매에도 어느 정도 한계가 있다 보니 더 확장성 있는 사업 모델인 클라우드 서비스와 스프링을 연결시키려고 했을 것이다.
그래서 클라우드 서비스 업체인 CloudFoundry를 인수했고, 결국 VMWare와 손을 잡았다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이러한 사업 배경과는 별도로 스프링과 클라우드를 연관시키는 것이 기술적으로 무리 수는 아닌지, 그 연결에 어떤 노력을 하고 있는지 살펴보아야 할 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스프링_포트폴리오의_확장과_클라우드&quot;&gt;스프링 포트폴리오의 확장과 클라우드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;VMWare의 인수 이후로, 스프링 포트폴리오의 발전 속도는 더욱 빨라 지고 있다. 최근 &lt;a href=&quot;http://www.springsource.org/spring-android&quot;&gt;Spring-Android&lt;/a&gt;, &lt;a href=&quot;https://github.com/springsource/spring-hadoop&quot;&gt;Spring-Hadoop&lt;/a&gt;같은 프로젝트를 보면 자바 기술 중 스프링이 건드리지 않은 영역이 바로 생각나지 않을 정도이다.
이 중 클라우드와 관련이 있는 기술들은 아래와 같이 나누어 정리해 볼 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째, 대용량 처리 미들웨어를 직접 스프링에서 인수한 것들이다.
AMQP(Advanced Message Queuing Protocol) 바탕의 메시징큐 솔루션인 &lt;a href=&quot;http://www.rabbitmq.com/&quot;&gt;RabbitMQ&lt;/a&gt;와 데이터그리드 기술인 &lt;a href=&quot;http://www.springsource.com/products/data-management&quot;&gt;GemFire&lt;/a&gt;가 여기에 해당한다.
비동기 처리와 데이터 캐시는 대용량 처리가 많은 클라우드 환경에서 더욱 많이 쓰이는 기술이 될 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 분산 저장소에 대한 API의 래핑(Wrapping)을 제공하는 것이다.
&lt;a href=&quot;http://www.springsource.org/spring-data&quot;&gt;Spring-data&lt;/a&gt;라는 프로젝트 아래에서 많은 저장소를 위한 프로젝트들이 진행되고 있다.
이미 Redis, Riak, CouchDB , MongoD, Neo4j등을 지원한다. 물론 꼭 스프링에서 지원해 주지 않아도 그런 저장소를 쓰는 데에는 문제가 없다.
그런 프로젝트의 의도는 JDBC를 바로 사용하는 것보다 Spring-jdbc를 사용하는 것처럼, 저장소 API도 스프링의 설정과 프로그래밍 방식으로 API를 편리하고 일관성 있게 사용하도록 도와 주는 것이다.
물론 저장소 API들이 JDBC만큼 불편하지는 않겠지만, Template-callback 패턴으로 반복된 Try-catch를 줄일 수 있는 부분 등 개선점이 없지는 않을 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 스프링 3.1에 추가되는 캐시 추상화도 분산 캐시 솔루션을 편하게 활용하도록 해 준다.
트랜잭션 처리 방식과 유사하게 애너테이션으로 캐시할 대상과 키를 지정하는 기능이다.
EhCache에 대한 구현체는 기본 제공되고 SPI(Service Provider Interface)에 맞추면 다양한 캐시가 이 스펙에 맞추어서 활용될 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;셋째, 모니터링 기술이다.
클라우드 환경에서는 물리적인 서버에 대해서 사용자가 신경을 쓰지 않더라도 실제로 많은 서버와 미들웨어 인스턴스를 쓰게 된다.
그리고 사용량으로 요금이 결정되므로, 리소스 사용량을 지속적으로 확인할 필요도 생길 수 있다.
사용자가 쓰는 패턴을 빨리 파악을 하는 것도 비용 대비 효과를 판단하는데 도움이 된다.
그래서 모니터링이 전통적인 시스템보다 더욱 중요하다. 스프링소스에서 모니터링 도구인 Hyperic을 인수하고 Spring-insight 같은 도구들을 만들고 있는 것이 이에 대한 대응으로 보여 진다.
Spring-insight에서는 Spring MVC의 Controller 정보 등 스프링에만 특화된 정보도 제공하는데, 모니터링이 독립적인 영역이지만 스프링을 썼을 때에 이득을 더 부각시키려는 의도일 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메시징큐, 데이터 저장소와 캐시, 모니터링 기술들은 기존 환경에 설치해서 사용할 수도 있지만,클라우드 서비스에서 제공하는 기술 스택에 포함되기도 한다.
클라우드 서비스에서도 기본으로 제공되는 솔루션들은 중요한 경쟁력이다.
아마존 클라우드에는 메시징 큐로 SQS(Simple Queue Servcie), 저장소로 SimpleDB가 있고, Google App Engine에서는 캐시로 Memcache, 저장소로 빅테이블에 바탕을 둔 DataStore를 쓸 수 있다.
VMWare가 제공하는 클라우드 솔루션인 VFabric에는 TcServer의 세션 클러스터링을 Gemfire로 쓰고, Hyperic으로 모니터링을 하도록 구성된다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최근 스프링 포트폴리오의 확장 중 많은 부분은 클라우드 시대에 맞춘 기술 투자라고 볼 수 있다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스프링과_클라우드_이식성cloud_portability&quot;&gt;스프링과 클라우드 이식성(Cloud Portability)&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링원 2010 컨퍼런스에서는 VMWare의 클라우드에서 돌아가는 애플리케이션이 Google App Engine에서 똑같이 돌아가는 데모를 보여 줬다.
JVM의 이식성을 생각한다면 어떻게 보면 당연한 결과일 수도 있는데, 이것이 어떤 의미가 있을까?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;클라우드를 도입하려는 쪽에서는 미래에 일어날 수 있는 다양한 상황들을 감안해야 한다.
애플리케이션을 레거시 서버에서 클라우드로, 클라우드에서 다른 클라우드로, 클라우드에서 다시 기존 방식의 서버로의 이전하는 모든 경우가 충분히 일어날 수 있다.
그렇다면 애플리케이션이 특정 실행 환경에 종속적인 부분이 많아 지는 것은 애플리케이션의 소유자에게는 큰 짐이 된다.
그래서 특정 클라우드에 애플리케이션이 묶여 버린다는 것은 얼핏 생각하면 클라우드 사업자에게 유리한 것 같지만, 클라우드의 잠재 사용자에게는 초기의 클라우드 도입을 망설이게 해서, 사용자층이 넓어지는데 부정적인 요인이 된다.
클라우드로 이사하는데 드는 비용이 많다면 기존 애플리케이션을 올리지도 않을 것이다.
그리고 이사 한 후에도 빠져 나오기 힘들다면 사용자가 가격 정책 협상에 불리한 위치가 된다.
그래서 클라우드 사업자는 클라우드 사용자가 기존 애플리케이션을 작은 수정으로 클라우드에 올릴 수 있고, 이사 나갈 때도 쉽게 옮길 수 있다는 것은 강조하는 편이 초기에 시장을 넓히는 데에는 유리할 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 클라우드 환경이 정말 기존 애플리케이션을 똑같이 받아줄 수 있을까? 우선 웹애플리케이션이 올라갈 WAS부터 그러기가 쉽지 않다.
클라우드 서비스에서는 한정된 종류의 WAS가 제공된다.
Google App Engine은 Jetty를 수정해서 쓰고 있고, VMWare가 관여하는 클라우드인 CloudFoundry, VMForce, VFabric은 당연히 Tomcat의 상용판인 Tc Server를 제공하고 있다. 거기다 클라우드에 올라가는 JVM이나 WAS는 지원하는 스펙이 제약된다. Google App Engine에서는 파일을 직접 쓰지 못하고, 스레드나 소켓을 생성할 수 없다.
서블릿 스펙에서도 ServletContext.getNamedDispatcher 을 호출해서 디폴트 서블릿의 이름을 알아내는 메서드가 제대로 동작하지 않는다.
보안 문제나 남용의 여지가 있는 부분은 지원하지 않는 것이다.
레가시 시스템을 클라우드 환경으로 옮기는 상황이라면 기존에 레거시 시스템에서 쓰던 WAS와 클라우드 위의 WAS의 종류가 다를 가능성이 높고, 기존의 WAS가 Java EE Server라면 더욱 그렇다.
더욱이 WAS 자체 혹은 애플리케이션에서 호출하는 기능까지도 제약된다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서 스프링이 클라우드 사업자들에게도 도움이 될 수 있다.
클라우드 소비자에게 지정된 WAS, 그것도 서블릿 스펙만 지원하는 WAS를 제공한다는 클라우드 서비스의 약점을 스프링이 상쇄해 줄 수 있다.
즉, JavaEE 스펙을 지원하는 서버가 없어도 Tomcat만으로도 객체의 라이프 싸이클 관리와 관계 주입, 선언적 트랜잭션 등을 쓸 수 있다.
그리고 JavaEE server를 쓰는 경우라도 서버가 제공하는 데이터 소스, 트랜잭션 서비스들을 스프링을 거쳐서 사용할 수 있다. 스프링을 통해 간접적으로 Java EE 스펙을 쓴다면, 나중에 Tomcat이나 Jetty로 WAS를 바꿀 때에도 애플리케이션의 적은 부분만 수정하면 된다.
예를 들면 JNDI로 데이터 소스를 찾아오는 부분은 DBCP로 바꾸고, JtaTransationManager를 쓰도록 선언된 Bean선언을 DataSourceTransactionManager으로 바꾸는 정도이다.
그렇게 때문에 스프링을 사용한 애플리케이션은 WAS간의 이식성이 높아지고, WAS선택의 폭이 넒어 진다.
WAS의 완충 지대 역할이라 할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한편으로는 스프링을 활용했을 때 WAS 같은 미들웨어에 대한 종속성은 적어지지만 프레임워크 자체에 종속성이 다시 생기기 때문에, 그것 또한 이식성을 줄이는 일이 아니냐는 생각을 할 수 있다.
하지만 굳이 둘 중에 종속성을 가져야 한다면, WAS보다는 프레임워크에 종속되는 편이 더 낫다고 생각한다.
프레임워크는 하나의 WAS 위에서 여러 개가 공존할 수도 있어서 점진적으로 바꿔나가기도 쉽다.
그리고 애플리케이션이 의존하는 부분을 WAS보다는 프레임워크에 두는 것이 유연성 측면에서는 유리하다.
프레임워크의 버전 업그레이드나 특정 라이브러리 변경은 WAS에 대한 업그레이드보다 간편하기 때문이다.
jar파일을 바꾸는 일만 생각한다면 프레임워크의 업그레이드는 Maven의 pom.xml에서 버전 선언 몇 줄만 바꿔 주면 된다.
반면 WAS는 설치 자동화가 되어 있다면 간편하게 모든 서버에 한꺼번에 복사할 수도 있겠지만, 아무래도 프레임워크 업그레이드 보다는 부담되는 일이다.
그리고 스프링은 스프링에 종속적이기 않게 코드를 작성하는 방법들을 많이 제공하고 있다.
&lt;code&gt;@Inject&lt;/code&gt; 같은 표준 애너테이션이 그 예이다.
이를 적절히 활용한다면 프레임워크에 대한 종속성도 다소 덜어낼 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세일즈포스닷컴, 구글은 스프링소스를 소유한 VMWare와 함께 스프링을 통한 클라우드 이식성(Cloud Portability)을 강조하고 있다.
어떻게 보면 경쟁 관계에 있는 이들이 한 목소리를 내고 있는 것은 초기 시장 확대가 무엇보다 중요하기 때문일 것이다.
그리고 사용자들을 자신들의 플랫폼 만으로 가두는 전략보다는 원한다면 오갈 수도 있는 길을 열어 두는 것이 장기적으로는 이득이라는 믿음을 공유하고 있는 것 같다.
그렇게 해도 될 만큼 핵심 경쟁력인 인프라 기술 등에서는 자신이 있다는 해석도 할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;기술_포털로서의_스프링&quot;&gt;기술 포털로서의 스프링&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링과 클라우드가 연관되고 있는 또 하나의 측면은, 스프링이 자바 기술 생태계에서 일종의 ‘포털’(Portal) 역할을 하고 있어서라고 분석된다.
인터넷 포털은 많은 사용자들이 방문하고 여러 정보들을 모아서 사용자에게 일관된 UX를 제공한다.
CP(Contents provider)사가 콘텐츠를 포털과 제휴하는 것은, CP사 입장에서는 자사의 콘텐츠를 알릴 수 있고, 포털 입장에서는 방문자에게 풍부한 콘텐츠를 제공해서 트래픽을 더 늘릴 수도 있다는 점에서 양측 모두에 이득이 된다.
그리고 사용자는 포털의 일관된 접근 경로와 UX로 다양한 콘텐츠를 접할 수 있다.
예를 들면 네이버의 휘발류 가격 정보는 석유공사에서 운영하는 Opinet에서 데이터를 가지고 오는 것이지만, 사용자는 네이버 검색창을 통해서 다른 콘텐츠를 볼 때와 같은 화면 스타일과 사용 방법으로 콘텐츠에 쉽게 접근할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;div class=&quot;title&quot;&gt;포털의 UI 일관화&lt;/div&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/spring-cloud/portal.png&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마찬가지로 스프링은 많은 개발자들이 사용하는 기술이고, 다양한 기술들을 조율해서 일관된 설정 방식과 명명 규칙, API 스타일을 제공한다.
API는 Application Programming Interface이니 포털의 User interface가 최종 사용자가 보는 화면이라면 개발자들이 보는 interface는 프레임워크와 라이브러리의 API라 할 수 있겠다.
그리고 스프링과 제휴하는 요소 기술의 보유 업체들은 CP사와 같이 자사의 기술을 알려서 사용자를 늘릴 수 있는 기회를 얻는다.
그리고 스프링소스는 스프링과 연결되는 기술 생태계를 더 풍요롭게 만든다는 이득을 얻는다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링은 다양한 기술을 같은 API로 추상화 시켜서 유연성을 주거나, 비슷한 스타일로 정리해서 개발자들에게 초기 학습 비용을 줄여 준다.
예를 들면 데이터그리드인 Gemfire에서 기존의 스프링의 DB 트랜잭션 관리 인터페이스에 맞춘 GemfireTransactionManager를 제공하는 것이 있다.
그리고 Spring-amqp 같이 최근 추가된 프로젝트도 클래스명, 인터페이스명, 메서드명은 스프링에 익숙한 사람이면 처음 보아도 친숙한 스타일을 느낄 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;VMWare가 스프링소스를 인수한 일이라던지, 세일즈포스닷컴과의 제휴나 Neo4j, Terracotta같은 저장소, 캐시 기술과 스프링이 연결되는 것은 기술적인 시너지를 기대한 측면도 있다.
그러나 그것도 스프링이 많은 사람들에게 익숙한 기술이고, 사람들을 모을 수 있기에 성사된 일들이다.
어느 새 VMWare가 자바 개발자들에게 이전보다 친숙한 기업이 되었으니 스프링소스 인수로 홍보 효과는 충분히 성공을 한 듯하다.
물론 그리고 단순한 홍보 수단 이상의 가치가 있으려면, 소비자들에게  시너지를 체감하게 하는 과제가 남아 있긴 하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;비침범적인_설계와_클라우드_시대&quot;&gt;비침범적인 설계와 클라우드 시대&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링이 처음 만들어 졌을 때 클라우드 시대를 염두에 두었을 리는 없다.
급작스런 시대의 변화에 스프링도 적응을 해야 하는 상황인데, 현재까지는 클라우드 업체 사이에 활발한 제휴 대상이 되고 있다.
인기 있는 기술이라서 홍보를 위해서 제휴를 하는 측면도 있겠지만, 그래도 스프링이 주는 유연성과 이식성이 클라우드에서도 의미가 있다는 평가를 받아서 투자와 협업 대상이 되었을 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 현상의 가장 근본적인 이유는 인프라성 코드와 업무 로직 코드를 침범적이지 않게 한다는 스프링의 설계 철학에서 비롯된다고 생각한다.
스프링에서 강조하는 POJO(Plain Old Java Object) 방식의 개발은 특별한 규약에 의존이 없는 자바 코드가 핵심 로직을 담당해서 실행 환경에 덜 의존적인 코드를 만든다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예를 들면 트랜잭션 처리 같은 인프라성 코드는 WAS가 제공하는 JTA(Java Transaction API) 같은 규약을 쓸 수도 있다.
그렇게 특정 미들웨어에 의존적인 코드는 최대한 한 곳으로 모으는 것이 향후 그 미들웨어가 바뀌었을 때 더 적은 수정을 유발한다.
스프링에서는 JTA에 중립적인 트랜잭션 API(PlatformTransactionManager)를 만들고, 이를 AOP를 통해서 사용해서 결국 JTA를 쓰는 코드를 한 곳에 모으고, 한 두줄 수정으로 다른 방식으로도 바꿀 수 있게 되었다.
다른 예로 Spring security가 Google App Engine의 로그인 인증에도 쓰일 수 있는 이유도  사용자 정보 저장소 인프라에 의존하는 부분이 잘 추상화되어 있기 때문일 것이다[주6].&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;거창하게 &apos;클라우드 이식성 &apos;을 위해서가 아니고, 시스템의 변화가 있을 때 적은 수정으로 대응할 수 있도록 고려를 하는 것은 설계의 기본이다.
역할과 책임이 잘 구분된 설계는 시대와 환경을 초월해서 의미가 있다.
결국 유연성, 이식성이라는 것은 잘된 모듈화의 일반적인 결과이지 꼭 특별한 기술을 적용해야 얻어지는 것은 아니다.
스프링의 AOP니 Dependency Injection이니 하는 기술들도 결국 그런 일을 돕기 위해서 존재하는 것일 뿐이다.
스프링 이전에도, 스프링 이후에도 이런 모듈화는 중요하고, 스프링이 클라우드 시대에서도 유행하고 있는 것은 이런 보편적인 설계 원리를 잘 지켰기 때문이라고 할 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;클라우드 시대에 애플리케이션 개발은 얼마나 달라질까?
사용하는 저장소나 미들웨어 같은 인프라는 많이 달라진다.
클라우드에 들어오는 새로운 구성요소들에 잘 적응을 하는 것이 처음에는 생산성을 결정하는 요인이 될 것이다.
그러나 그렇다고 애플리케이션을 개발하는 방식이 크게 달라진다고는 생각하지 않는다.
잘 모듈화되어서 역할과 책임이 잘 구분된 코드는 클라우드 시대까지 살아남을 것이고, 그런 코드는 클라우드 시대에도 수정, 추가 비용을 적게 하고, 클라우드 이후 시대까지도 남겨 질 것이다.
프레임워크의 코드이든, 응용 개발자가 짜는 애플리케이션의 코드이든 마찬가지로 말이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;덧붙여서, 그렇게 관심과 역할이 잘 정리된 좋은 코드를 만들었다는 것을 무엇으로 증명할 것인가?
바로 테스트 코드를 짜 보는 것이다. 인프라를 관리하는 코드와 업무 규칙의 코드가 섞여있다면, 테스트 코드를 짜기가 힘들 것이고, 그런 코드는 앞으로의 변경에도 더 많은 비용이 드는 코드가 되고 살아남기 힘든 코드가 된다.
특정 프레임워크를 썼다고 좋은 설계가 당연히 바로 나오지는 않는다.
스프링을 쓰면서 테스트하기 쉬운 코드를 짜고 있는지를 돌아보는 것이 스프링을 잘 쓰고 있는지를 가장 잘 확인하는 방법이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링소스가 클라우드와 관련한 활발한 활동들을 하는 것은 확장성 있는 사업 모델을 찾기 위한 돌파구였고, 포트폴리오와 강화와 각종 제휴로 현재까지는 클라우드를 지원하는 기술 쪽으로도 빠른 발전을 보이고 있다. 그리고 스프링이 제공하는 이식성 덕분에 제한된 WAS를 제공하는 클라우드에 유리한 점이 있고, 넓은 사용자층을 가진 기술 생태계의 포털 역할로 그 가치를 인정받은 것으로 분석된다. 그런데 스프링이 클라우드 시대에도 흥행하고 적응하고 있는 가장 근본적인 이유는 애플리케이션의 핵심 로직과 인프라를 담당하는 부분을 구분한 설계를 하는데 도움이 되는 기술이기 때문이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;10년전 한 개인이 만들었던 코드조각이였을 뿐인 스프링이 이제는 업계 흐름을 좌우하는 기술이 되었다. 스프링의 예처럼, 오픈소스는 더 이상 잉여시간이 넘치는 개인들의 습작이 아니고, 기업이 전략적 협업을 하는 매개체이다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;올해에도 스프링의 소식들이 마구 쏟아질 것이고 그 중 상당 부분은 클라우드와 관련이 있을 것이다. 그들의 전략이 자바 생태계의 참여자 모두에게 혜택이 될 수 있는 방향으로 이어지기를 바란다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;주&quot;&gt;주&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;[주1] 관련 내용이 인용된 기사 &lt;a href=&quot;http://www.bloter.net/archives/15878&quot; class=&quot;bare&quot;&gt;http://www.bloter.net/archives/15878&lt;/a&gt; . 물론 스프링 기술 중 작은부분만 활용하고 있어도 스프링을 쓰는 것으로 집계되었을 것이라고 생각된다.
[주2] 당시 논의 되었던 라이선스 정책에 대해서는  &lt;a href=&quot;http://toby.epril.com/?p=440&quot; class=&quot;bare&quot;&gt;http://toby.epril.com/?p=440&lt;/a&gt; 에 자세히 설명되어 있다.
[주3]로드존슨은 Covalent 인수 배경에 대해서 아래와 같이 밝히고 있다.
&lt;a href=&quot;http://blog.springsource.com/2008/01/29/some-decisions-are-easy-%E2%80%93-like-springsource-acquiring-covalent/&quot; class=&quot;bare&quot;&gt;http://blog.springsource.com/2008/01/29/some-decisions-are-easy-%E2%80%93-like-springsource-acquiring-covalent/&lt;/a&gt;
[주4] 인수 가격과 원화 환산 금액은 아래 링크를 참조했다
&lt;a href=&quot;http://younghoe.info/1192&quot; class=&quot;bare&quot;&gt;http://younghoe.info/1192&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;[주5]로드존슨의 사업 모델에 대한 고민은 아래에 발표자료에 언급되어 있다.
&lt;a href=&quot;http://gotocon.com/dl/jaoo-brisbane-2010/slides/RodJohnson_bKeynotebThingsIWishIdKnown.pdf&quot; class=&quot;bare&quot;&gt;http://gotocon.com/dl/jaoo-brisbane-2010/slides/RodJohnson_bKeynotebThingsIWishIdKnown.pdf&lt;/a&gt;
[주6] &lt;a href=&quot;http://blog.springsource.com/2010/08/02/spring-security-in-google-app-engine/&quot; class=&quot;bare&quot;&gt;http://blog.springsource.com/2010/08/02/spring-security-in-google-app-engine/&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>SpringOne2GX 2010 (4) Spring Roo 관련 발표들</title>
      <link>https://blog.benelog.net//2710123.html</link>
      <pubDate>Thu, 11 Nov 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2710123.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;작년 SpringOne에서도 Spring Roo에 대한 발표가 있었고, Google IO 등 다른 컨퍼런스에서도 꾸준히 Spring Roo는 홍보되었지만, 이번 SpringOne에서는 부쩍 그 비중이 높게 느껴졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;키노트가 있었던 주행사장에는 Spring Roo, Spring, SpringSource, Groovy, Grails의 5개의 로고가 조명으로 비추어져 있었습니다. Groovy-Grais의 관계처럼 Spring-SpringRoo의 관계를 연상시켜서, Spring의 대표 기술로  홍보하려는 전략으로 보였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://lh3.ggpht.com/_oJrmz3UkGJk/TMi7gUjZNII/AAAAAAAAC0A/hSU3s8E2XCk/s640/PA220055.JPG&quot; alt=&quot;_oJrmz3UkGJk/TMi7gUjZNII/AAAAAAAAC0A/hSU3s8E2XCk/s640/PA220055.JPG&quot; width=&quot;640&quot; height=&quot;480&quot; title=&quot;_oJrmz3UkGJk/TMi7gUjZNII/AAAAAAAAC0A/hSU3s8E2XCk/s640/PA220055.JPG&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://lh6.ggpht.com/_oJrmz3UkGJk/TMi7hDFNR_I/AAAAAAAAC0A/mWjWPk7bMPE/s912/PA220058.JPG&quot; alt=&quot;_oJrmz3UkGJk/TMi7hDFNR_I/AAAAAAAAC0A/mWjWPk7bMPE/s912/PA220058.JPG&quot; width=&quot;465&quot; height=&quot;348&quot; title=&quot;_oJrmz3UkGJk/TMi7hDFNR_I/AAAAAAAAC0A/mWjWPk7bMPE/s912/PA220058.JPG&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 행사 기념품으로 나온 배찌에서도, Spring,SpringSource, Goorvy, Grails, Tomcat 등과 함께 Spring Roo의 로고가 박힌 것이 포함되었습니다. Security나 Batch같이 이미 현장에서 더 많이 쓰이고 있는 하위 프로젝트들도 있는데, Roo만 특별대우 한다는 느낌까지 들 정도였습니다.  다른 프레임웍 기술만과는 차별된 Spring만의 강점을 강조하기 위해서 Spring Roo가 전면에 나왔다고 생각됩니다.  스프링에서 지원하는 기술이 많아질 수록, API들을 전파하는 것도 쉽지 않을 것인데, Spring Roo를 통해서 사용할 수 있는 방법을 제공하면 코드가 자동생성 되므로 사용법이 더 간편해 보인다는 장점이 있을 것입니다. 그리고 Roo가 그렇게 새로운 API 전파 창구의 역할을 수행한다면 Roo를 직접 사용하지 않는 사람도 Roo가 생성해주는 코드를 샘플로 활용할 수도 있을 것입니다. Spring 3.0.4에 포함된 &amp;lt;mvc:default-servlet-handler/&amp;gt;가 Roo에 바로 반영된 것이나, Neo4j의 Roo addon등이 그 예입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컨퍼런스가 끝나고 몇일 뒤에 바로 Spring Roo 1.1.0 버전이 발표되었는데,  이번 컨퍼런스에서 1.1.0에 포함된 기능을 소개하는 발표가 많았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.springsource.com/2010/10/27/spring-roo-1-1-0-is-released/&quot; class=&quot;bare&quot;&gt;http://blog.springsource.com/2010/10/27/spring-roo-1-1-0-is-released/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OSGi, GWT, GAE-J 지원,  검색서버인 Apache Solr  지원, Database reverse engineering 등 많은 발전을 보여줍니다. 아래 포스트에 있는 지난 1년간의 Roo의 commit 내력을 시각화한 그림에서도 그런 변화가 표현되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://stsmedia.net/spring-roo-1-1-ga-released/&quot; class=&quot;bare&quot;&gt;http://stsmedia.net/spring-roo-1-1-ga-released/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;벤알렉스 아저씨가 열심히 개발을 하는 모습이 보이는군요.  이번 컨퍼런스에  벤알렉스는 참석하지 않았고, 벤알렉스가 진행하기로 한 발표의 일부는 로드존슨이 직접 진행했습니다. 아파서 못 왔다고는 말했는데, 1.1 출시를 얼마 안 앞두고 마무리 작업 때문에 못 온 것이 아닌가 하는 생각도 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 Roo 관련 세션 중에 Add-On 개발 관련 세션에 들어갔었습니다.
아래 URL에 있는 toString addon을 샘플소스로 보라고 했는데,  toString을 Addon도 크게 쉬워보이지는 않았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://git.springsource.org/roo/roo/trees/master/addon-tostring/src/main/java/org/springframework/roo/addon/tostring&quot; class=&quot;bare&quot;&gt;http://git.springsource.org/roo/roo/trees/master/addon-tostring/src/main/java/org/springframework/roo/addon/tostring&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장  흥미로운 이야기는 Roo에서 앞으로 iBatis, Spring jdbc 같은  JPA 이외의 Persistence 기술도 지원하겠다는 것이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Maven 멀티 프로젝트를 언제 지원할 수 있으냐는 질문이 세션 중에 나왔는데, 명확한 일정을 확답을 하지 못한 것으로 봐서는 가까운 시일 내에 가능해지지는 않을 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Roo 관련 발표 자료들은 아래에 공개되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.slideshare.net/schmidtstefan/new-persistence-features-in-spring-roo-11&quot;&gt;New Persistence Features in Spring Roo 1.1&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.slideshare.net/schmidtstefan/next-generation-spring-mvc-with-spring-roo&quot;&gt;Next Generation Spring MVC with Spring Roo&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.slideshare.net/schmidtstefan/spring-one2010addondev&quot;&gt;Spring Roo Add-On Development &amp;amp; Distribution&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java에서 XML없이 SQL개발하기</title>
      <link>https://blog.benelog.net//2708621.html</link>
      <pubDate>Mon, 8 Nov 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2708621.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;변경 이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2015/01/21 :  Eclipse에서 Groovy를 쓰기위한  plugin과 Maven 선언 부분을 현행화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2013/01/25 : &lt;a href=&quot;http://www.dpriver.com/pp/sqlformat.htm&quot; class=&quot;bare&quot;&gt;http://www.dpriver.com/pp/sqlformat.htm&lt;/a&gt; 의 캡쳐화면 추가 등&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2012/11/06 :  Eclipse에서 Groovy를 쓰기위한  plugin과 Maven 선언 부분을 더 편한 방식으로 수정&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;요약하면, Java의 여러 프레임웍은  XML안에 SQL을 넣는 방식을 지원하는데, 줄바꿈이 있는 문자열을 편하게 쓰게 해주는 따옴표 세 개문법 (&quot;&quot;&quot;)만 Java에 추가된다면 XML을 사용하는 목적을 충족시키면서도 XML로 인한 여러 단점들을 겪지 않아도 된다는 것입니다. 따옴표 세개는 Java에서 추가될 예정이지만, Groovy등에서는 이미 지원합니다. 지금이라도 SQL관리에만 Groovy를 쓰면 쿼리편집이 조금 더 편리해질만도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;xml로_sql을_관리할_수_있는_java_framework&quot;&gt;XML로 SQL을 관리할 수 있는 Java framework&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많은 Java 프레임웍들이 SQL구문들을 XML파일 안에서 코딩하게 되어 있습니다.
가장 대표적인 것이 iBatis입니다. 아래와 같이 SQL 구문, 파라미터를 운반하는 클래스,  쿼리의 결과가 담길 클래스를 XML안에 선언합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;    &amp;lt;select id=&quot;findByIsbn13&quot; parameterClass=&quot;string&quot; resultClass=&quot;book&quot;&amp;gt;
    SELECT  title,    author,     isbn13,     isbn10,     pages, content, imageUrl
    FROM book
    WHERE isbn13 = #isbn13#
    &amp;lt;/select&amp;gt;
    &amp;lt;select id=&quot;findByTitle&quot; parameterClass=&quot;string&quot; resultClass=&quot;book&quot;&amp;gt;
    SELECT  title,    author,     isbn13,     isbn10,     pages, content, imageUrl
    FROM book
    WHERE title = #title#
    &amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Hibernate와 JPA에서도 &quot;named query&quot;라는 개념으로, SQL을 따로 XML파일로도 뺄 수 있습니다. 아래는 Hibernate에서 SQL을 XML 파일안에 설정한 예입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;sql-query name=&quot;findBookByIsbn13&quot;&amp;gt;
    &amp;lt;return alias=&quot;book&quot; class=&quot;tdd.edu.domain.Book&quot;/&amp;gt;
   SELECT  title,    author,     isbn13,     isbn10,     pages, content,  imageUrl
   FROM book
    WHERE isbn13 = :isbn13
 &amp;lt;/sql-query&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Navie SQL과 Hibernate가 쓰는 HQL을 모두 .xml파일 안에 선언하는 것이 가능합니다. JPA를 사용해도 마찬가지로 JPA-QL, Native SQL을 Java에서 String으로 선언할 수도 있지만, XML 파일 안에 넣어도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring JDBC에서는 Jdbctemplate.execute 등의 메서드에서 SQL내용을 직접 문자열로 넘기게 되어있지만, Applicaton context 안에 쿼리를 저장해두고, 이를 사용하는 쪽에서 &lt;code&gt;java.util.Properties&lt;/code&gt; 같은 객체를 Dependency Injection 받아서 사용하면 iBatis처럼 XML로 쿼리가 관리됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;util:properties id=&quot;bookSqls&quot;&amp;gt;
    &amp;lt;prop key=&quot;findByIsbn13&quot;&amp;gt;
   SELECT  title,    author,     isbn13,     isbn10,     pages, content,  imageUrl
   FROM book
    WHERE isbn13 = :isbn13
    &amp;lt;/prop&amp;gt;
&amp;lt;/util:properties&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, Spring-jdbc나 Hibernate, JPA에서는 XML에 SQL을 저장하는 방식이 선택일 뿐이지만, iBatis 2.x에서는 반드시 XML안에 쿼리를 넣어야합니다.  myBatis라고 이제 이름이 갈라진 iBatis 3.x에서는 Annotation으로 쿼리를 지정할 수 있어서, .java파일 안에 문자열로 SQL에 넣어도 되기는 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;final String PERSIST_INFO =
“INSERT INTO simple_information(info_id, info_content) VALUES (#\{infoId}, #\{infoContent})”;

@Insert(PERSIST_INFO)
public int persistInformation(SimpleInformationEntity simpleInfo) throws Exception;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(예제는 &lt;a href=&quot;http://java.dzone.com/articles/mybatis-formerly-called-ibatis&quot; class=&quot;bare&quot;&gt;http://java.dzone.com/articles/mybatis-formerly-called-ibatis&lt;/a&gt;http://java.dzone.com/articles/mybatis-formerly-called-ibatis 에서)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, 이런식으로 쿼리까지 Annotation으로 지정하는 것에 대해서는 의견이 분분할 것 같고, 개인적인 생각으로는 Spring jdbc나 Hibernate처럼 필요하면 직접 메서드 시그니처에 직접 SQL을 문자열로 넘기는 방식이 훨씬 더 자연스럽다고 보여집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제점은&quot;&gt;문제점은?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;iBatis에서는 파라미터에 따라서 SQL이 다르게 구성되는 다이나믹 쿼리를 아래와 같이 선언합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;isEqual property=&quot;writerSelected&quot; compareValue=&quot;false&quot;&amp;gt;
  &amp;lt;isNotNull property=&quot;writerList&quot;&amp;gt;
    &amp;lt;iterate prepend=&quot; AND writer in&quot; property=&quot;writerList&quot;
       open=&quot;(&quot; close=&quot;)&quot; conjunction=&quot;,&quot;&amp;gt;#writerList[]#
    &amp;lt;/iterate&amp;gt;
  &amp;lt;/isNotNull&amp;gt;
&amp;lt;/isEqual&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;if, for문 처럼 조건,반복문들이 XML로 표현되어 있습니다. 이는 절차적 프로그래밍을 SQL로 하게 되어서 아래와 같은 단점이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;조건, 반복문에 해당하는 태그 문법을 별도로 배워야함&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;괄호&quot;\{}&quot;대신 열고 닫는 태그가 단락을 구분하기 때문에, 같은 조건,반복문을 코딩해도 Java 같은 범용언어에서보다 긴 코드가 나오게됨&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compile time의 validation범위가 더 줄어들게 됨.  getter, setter로 참조하게 될 속성명에 오타가 있어도 직접 실행해봐야지 오타를 알 수 있음.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java파일 밖이므로, Emma와 같은 Coverage 확인 툴로 실제 해당 절이 실행되었는지 확인할 수도 없음.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;왜_sql이_xml에_들어가게_되었을까&quot;&gt;왜 SQL이 XML에 들어가게 되었을까?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;직접 JDBC를 쓰면 Connection 관리와 Exception처리 등이 불편합니다.
그리고 JDBC의 Prepared Statement에서는 파라미터를 &quot;?&quot;를 표시하기 때문에 거기에 넘어가는 변수를 위치의 순서로 파악을 해야 합니다.
&quot;:id&quot;와 같이 named parameter를 넣을 수 있다면 훨씬 쿼리의 가독성이 높아집니다.
그래서 그러한 Jdbc의 미흡한 점들을 보완해주는 프레임웍들이 각광을 받았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데, Connection이나 Excpetion처리의 편의성, named parameter의 활용하고 싶다고 해서 반드시 XML로 SQL를 관리해야 하는 것은 아닙니다.
XML을 안 써도 되는 Spring의 JdbcTemplate에서도 그런 기능은 다 제공을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SQL이 한 파일에 모여있지 않으면  DBA한테 쿼리 검수를 맡기거나, 여러 SQL을 한번에 수정할 일이 있을 때 불편해 지기도 합니다.
그러나 그런 점도 SQL 내용을 상수로 선언하는  .Java 파일을 따로 분리하면 해결할 수 있습니다.
SQL을 보관하는 .java파일에 *SqlMap.java와 같은 명명규칙을 부여하고,  SQL 검수를 맡길 때 그 파일만 넘기면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또, 과거에는  .java파일 밖에 SQL이 있으면 SQL만을 수정을 했을 때는 다시 컴파일을 안 해도 된다는 장점이 강조되었습니다.
그러나, 요즘은 개발 PC에서는 Eclipse로, 서버에 배포할 때는 Ant나 Maven으로 빌드과정이 간편해졌고, 설정파일을 수정해도 파일의 복사를 위해 그런 배포과정을 똑같이 거쳐야 하므로, 컴파일이 필요없다는 것도 더이상 장점이 되지 못합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;XML에 SQL을 썼던 가장 핵심적인 이유는 .java파일에서는 줄 바꿈이 들어간 문자열을 편집을 하는 것이 불편했기 때문입니다.
Java 파일에서는 문자열이 한 줄이 넘어가면 아래와 같이 &lt;code&gt;+&lt;/code&gt; 기호를 이용해서 이를 연결해주는 방법 밖에 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static final String SELECT_BY_ISBN13 =
    &quot;SELECT name , id &quot;
    + &quot;FROM user &quot;
    + &quot;WHERE isbn13 = :isbn13 &quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보통 Toad와 같은 DB client 도구에서 SQL을 작성해서 프로그램에 붙여넣기도 하고, 디버깅 중에는 프로그램 내에 있는 SQL을 반대로 DB client 툴에 붙여넣어서 실행해보기도 하는데, 그 때마다 저렇게 줄바꿈마다 &quot;+&quot;가 있다면 쿼리 편집이 많이 번거로워집니다. 그래서 XML파일 안에 SQL이 있으면 줄바꿈이 있는 긴 문자열도 똑같이 붙여넣을 수 있기 때문에, SQL을 개발하는 작업이 훨씬 편해집니다.  이렇게 SQL이 XML안에 들어가다보니 동적쿼리를 만들기 위한 조건,반복문과 각종 파라미터 매핑 클래스등까지 다 XML에 포함되어 버렸고, 앞에서 말한 부작용들이 점점 드러나기시작했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;물론 Eclipse의 설정으로 .java 파일에 붙여넣기를 할 때는 &quot;+&quot;를 넣는 것과 같이 줄을 바꿀 때 필요한 작업들을 자동으로 할 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Windows-Preference-Java-Editor-Typing란의 &quot;Escape text When pasting into a string literal&quot;을 선택하고, 큰 따옴표 하나를 연 채 여러줄을 붙여넣으면, 알아서 줄이 바뀔 때는 &quot; + &quot; 기호를 넣어줍니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sql-without-xml/typing.png&quot; alt=&quot;typing.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 반대로 이런 여러줄의 String을 DB 접속 툴에 붙여 넣을 때도 &lt;a href=&quot;http://squirrel-sql.sourceforge.net/&quot;&gt;Sql-Squirrel&lt;/a&gt;이나 Toad 같은 툴에서는 그런 &quot;+&quot;와 같은 기호를 제거해 주는 기능이 있기도 합니다.  그리고 웹으로 이런 변환을 해주는 사이트도 있습니다.  &lt;a href=&quot;http://www.dpriver.com/pp/sqlformat.htm는&quot; class=&quot;bare&quot;&gt;http://www.dpriver.com/pp/sqlformat.htm는&lt;/a&gt;  여러줄로 된 SQL문장을 Java, C#, Delphi, PHP등 다양한 언어의 문자열 선언으로 변환해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sql-without-xml/sql-formatter.png&quot; alt=&quot;sql-formatter.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 편집을 도와주는 설정이나 도구들을 쓰더라도  중간 변환과정에서 몇번의 키 입력과 클릭이 필요해서 아예 그런 과정이 없는 것보다는 번거롭게 느껴집니다. 그래서 문자열 전체를 중간 변환과정없이 편집할 수 있는 XML에 SQL을 선언하기 시작했다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;대안으로_groovy로_따옴표_3개_문법을_이용해서_sql_관리하기&quot;&gt;대안으로 Groovy로 따옴표 3개 문법을 이용해서 SQL 관리하기&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Python, Groovy, Scala, Ruby에서 이미 지원하고 있는 &apos;따옴표  3개짜리 문자열 선언&amp;#8217;이 Java에도 포함된다면 여러줄의 문자열을 따로 편집하는 불편함을 겪지 않아도 됩니다. 아래와 같이 중간에 줄바꿈이 있어도 전체 SQL 내용이 끊어지지 않고 들어갑니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt; public static final String SELECT_BY_ISBN13 =    &quot;&quot;&quot;

  SELECT  title,    author,     isbn13,     isbn10,     pages, content,  imageUrl
   FROM book
   WHERE isbn13 = :isbn13

&quot;&quot;&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 따옴표 3개는 이미 JDK7에 포함되는 것이 제안된 상태인데, JDK에 포함될 실험적인 내용을 구현해보는 &quot;Kijaro&quot;라는 프로젝트에서는 &lt;a href=&quot;https://docs.google.com/View?docid=d36kv8n_32g9zj7pdd&quot;&gt;Enhanced String Handling for Java&lt;/a&gt;라는 이름으로 이 명세를 다루고 있습니다. 그러나, 내년 중반기 쯤에 JDK7에 포함되어 발표될 예정인, java의 문법 개선내용을 주로 담고 있는 &lt;a href=&quot;http://openjdk.java.net/projects/coin/&quot;&gt;project coin&lt;/a&gt;에서는 아직 이를 찾아볼 수 없어서, 언제 Java에 반영될지는 아직 미지수입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다면 Java에서 따옴표 3개를 지원해주기 전까지는 계속 XML의 불편함을 감수해야 할까요? 저는 이미 이 문법을 지원하는 Groovy를 SQL관리 용도로 사용해볼만 하다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groovy를 사용하기 위해서는 Eclipse와 Maven에 아래 설정만 해주면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1.Eclipse에서 Groovy plugin 설치&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Update site:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;4.2 (Juno) : &lt;a href=&quot;http://dist.springsource.org/release/GRECLIPSE/e4.2/&quot; class=&quot;bare&quot;&gt;http://dist.springsource.org/release/GRECLIPSE/e4.2/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.7 (Indigo) : &lt;a href=&quot;http://dist.springsource.org/release/GRECLIPSE/e3.7/&quot; class=&quot;bare&quot;&gt;http://dist.springsource.org/release/GRECLIPSE/e3.7/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.6 (Helios) : &lt;a href=&quot;http://dist.springsource.org/release/GRECLIPSE/e3.6/&quot; class=&quot;bare&quot;&gt;http://dist.springsource.org/release/GRECLIPSE/e3.6/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groovy-Eclipse Configurator for M2Eclipse도  설치가 필요한데, 2번 과정에서 pom.xml에 빨간 줄이 뜨면 Ctrl +1 을 눌러서도 설치할수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2.pom.xml에 Groovy를 compile할 수 있는 plugin과 runtime dependency 추가&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Groovy를 Compile하는 Maven plugin은  &lt;a href=&quot;http://docs.codehaus.org/display/GMAVEN/Home&quot;&gt;Gmaven&lt;/a&gt; 과 &lt;a href=&quot;http://groovy.codehaus.org/Groovy-Eclipse+compiler+plugin+for+Maven&quot;&gt;Groovy-Eclipse Compiler Plugin For Maven&lt;/a&gt;이 있습니다. 후자가 Eclipse 최신버전의 me2와 더 나은 궁합을 보여줘서 컴파일을 할 때는 후자를 선택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(1)Dependencies에 선언 추가&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.codehaus.groovy&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;groovy-all&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.4.5&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(2)build-plugins 에 아래 내용 추가&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;               &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.3.2&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;compilerId&amp;gt;groovy-eclipse-compiler&amp;lt;/compilerId&amp;gt;
                    &amp;lt;meminitial&amp;gt;128m&amp;lt;/meminitial&amp;gt;
                    &amp;lt;maxmem&amp;gt;512m&amp;lt;/maxmem&amp;gt;
                    &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
                    &amp;lt;encoding&amp;gt;utf-8&amp;lt;/encoding&amp;gt;
                &amp;lt;/configuration&amp;gt;
                &amp;lt;dependencies&amp;gt;
                    &amp;lt;dependency&amp;gt;
                        &amp;lt;groupId&amp;gt;org.codehaus.groovy&amp;lt;/groupId&amp;gt;
                        &amp;lt;artifactId&amp;gt;groovy-eclipse-compiler&amp;lt;/artifactId&amp;gt;
                        &amp;lt;version&amp;gt;2.7.0-01&amp;lt;/version&amp;gt;
                    &amp;lt;/dependency&amp;gt;
                &amp;lt;/dependencies&amp;gt;
            &amp;lt;/plugin&amp;gt;

&amp;lt;plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3.Groovy 사용&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 New&amp;#8594; groovy class를 선택하여서 java 파일 작성하듯이 클래스를 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sql-without-xml/new-groovy-class.png&quot; alt=&quot;new-groovy-class.png&quot; title=&quot;new_groovy_class.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 문법을 그대로 쓸 수 있으니 따옴표 3개를 쓸 수 있다는 점만 다르다고 생각해도 됩니다. 아래와 같이 .groovy 파일 안에 들어간 SQL이 색깔도 다르게 표시되어 비교적 가독성이 높게 표시되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sql-without-xml/groovy-sqls.png&quot; alt=&quot;roovy-sqls.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 다음 DAO 등 SQL을 호출하는 쪽에서는 이 상수 문자열을 바로 참조합니다. 상수 선언이 되어 있으니 아래와 같이 오타를 쳐도 미리 알려주고, Ctrl + Space를 치면 자동완성도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sql-without-xml/typing-error.png&quot; alt=&quot;typing-error.png&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Dynamic SQL의 경우에도 직접 Java안에서 if문으로 써서 적어주면 됩니다. 아래와 같이 EclEmma 같은 도구로 coverage를 측정하면, 실제 실행되지 않은 조건분기도 눈으로 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/sql-without-xml/coverage.png&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 코드를 Spring-JDBC를 사용했는데, 필요하다면 Hibernate나 &lt;a href=&quot;http://commons.apache.org/dbutils/apidocs/index.html&quot;&gt;apache commons DBUtils&lt;/a&gt;에서도 적용 가능한 방법입니다. 다만 Hibernate에서는 Criteria 같은 것을 이용하면 문자열로 길게 쓰는 쿼리가 많이 나오지는 않을 것으로 예상합니다. 그리고 myBatis(iBatis 3.0)의 Annotation으로 지정하는 쿼리 문자열에서도 똑같이 참조할  수 있습니다. static final String으로 선언된 문자열 상수만 쓰는 것이기 때문에 Groovy의 성능문제도 걱정할 필요가 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;단점과 꺼림직함도 물론 있습니다. 별도의 Eclipse plugin을 설치해야 하기 때문에, 이미 많은 수의 Plugin을 설치해서 Eclipse가 무겁다고 느껴지는 개발환경에서는 다소 부담이 될지 모른다는 점입니다. Groovy가 거의 java와 같은 문법을 지원하기는하지만, 이 문법 하나 때문에 새로운 언어를 도입하는 것이 과하다고 느끼시는 분들도 계실 것입니다. 이 문법을 위해서 Groovy의 다른 부분은 쓰지 않는데도 Runtime에 Groovy의 라이브러리를 올리는 것이 부담스러울 수도 있습니다. (따옴표 3개와 같은 Groovy만의 문법을 바이트코드로 변환하는 작업은 Compile time에 이루어지지만, Groovy로 선언한 객체는 groovy.lang.GroovyObject를 상속해야 하기 때문에, 다른 기능을 쓰지 않더라도 Runtime에서 추가 라이브러리가 필요합니다.) 그런 분들은 언제가 될지는 몰라도 Java에서 따옴표 3개를 지원하는 때까지 기다리는 것이 좋으실듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SQL을 XML에 쓰는 것이 전혀 불편하지 않다고 느끼신다면 계속 하던대로 개발을 하면 되겠지만, 저는 위의 시도가 조금이라도 더 편한 개발환경을 만든다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>SpringOne2GX 2010 (3) Spring and Java EE 6,Synergy or Competition?</title>
      <link>https://blog.benelog.net//2703581.html</link>
      <pubDate>Fri, 29 Oct 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2703581.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;논쟁에_응하다&quot;&gt;논쟁에 응하다&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2010년 10월 4일, &lt;a href=&quot;http://www.theserverside.com/&quot;&gt;http://www.theserverside.com&lt;/a&gt; 에는 이제 프레임웍의 시대는 가고, Spring에서 Java EE6로 옮겨야 한다는 주장을 담은 글이 올라왔고, 뜨거운 논쟁거리가 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.theserverside.com/news/thread.tss?track=NL-461&amp;amp;ad=790558&amp;amp;thread_id=61023&amp;amp;asrc=EM_NLN_12619056&amp;amp;uid=2873925&quot;&gt;Moving from Spring to Java EE 6: The Age of Frameworks is Over&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;과거 Java EE 스펙의 부족함 때문에 Spring이 떠올랐지만, 이제 Java EE 6에서는 그런 것들을 다 극복을 했으니 Java EE6로 갈아타자는 내용이였습니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;이 글의 댓글에서부터 많은 반론이 올라왔고, 별도의 포스트로 쓰여진 아래와 같은 글들도 있었습니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://raibledesigns.com/rd/entry/re_moving_from_spring_to&quot; class=&quot;bare&quot;&gt;http://raibledesigns.com/rd/entry/re_moving_from_spring_to&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jandiandme.blogspot.com/2010/10/spring-vs-java-ee-and-why-i-dont-care.html?utm_source=feedburner&amp;amp;utm_medium=twitter&amp;amp;utm_campaign=Feed%3A+jandiandme2+%28J+and+I+and+Me%29&amp;amp;utm_content=Twitter&quot;&gt;Spring vs. Java EE and Why I Don&amp;#8217;t Care&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;반론들에 포함된 공통적인 내용들은 아래와 같습니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;크게 볼 때, Spring과 Java EE6 는 비슷한 프로그래밍 모델을 가진 부분이 많다.Annotation을  @Stateless을 쓸지, @Component 쓸지는 프로젝트의 성공여부를 결정지을 만큼 중요한 차이는 아니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그러나 Spring이 Tomcat, Jetty를 비롯한 더 많은 WAS에서 실행될 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Batch, Integration, Spring Roo 같은  Java EE 스펙이 미치지 못하는 영역을  스프링에서는 제공하고 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JavaEE 진영과 스프링 쪽의 갈등은 이미 뿌리가 깊습니다.  스프링의 시초가 된 코드가 있는 &quot;Expert One-on-OneJ2EE Design and Development&quot;책에는 EJB의 단점들이 날카롭게 지적되어 있고, 로드존슨과 유겐할러가 써서최초로 스프링을 언급한 책은 이름부터가 &quot;Expert One-on-One J2EE Development withoutEJB&quot;였습니다. &quot;with Spring&quot;도 아닌 &quot;without EJB&quot;인 것이죠. EJB를 적용했을 때의 실패경험 때문인지로드존슨은 EJB에 맺힌 것이 많아 보이고, 그런 것들이 앞의 저서들을 저술한 동기를 더 강하게 하지 않았을까하는 생각도듭니다.  그래도 J2EE 시절만 해도 표준 스펙은 OS나 DB서버와 같이, low level의 기술을 제공하는 컨테이너로인식이 되었는데, Java EE5 이후  실사용자 레벨의 컴퍼넌트 모델을 제공하기 시작하면서 JavaEE와 Spring은경쟁관계로 인식이 됩니다. 사실 스펙을 정의하는 JavaEE와 구현기술인 Spring을 동등하게 비교할 수도 없고,Spring에서도 JavaEE스펙을 지원하는 부분이 있습니다. 하지만 JavaEE스펙을 더 지지하는 진영에서는 Spring을사유기술일 뿐이고 JavaEE에 근거한 기술은 표준이라는 것을 계속 강조하면서 스프링을 공격하고 있습니다.  로드존슨은 이전에올린 &lt;a href=&quot;http://blog.springsource.com/2009/04/14/enterprise-java-and-the-american-motors-gremlin/#comment-164086&quot;&gt;블로그 포스트의 댓글&lt;/a&gt;에서 오픈소스 기술인 Spring을 사유(proprietary)라고 표현한 것에 대해서는 그것은 의심스러운 용어일 뿐이라고 말한적도 있습니다.  (In any case, &quot;proprietary&quot; is a questionable term when we&amp;#8217;retalking about open source. )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마침 SpringOne2GX 컨퍼런스 직전에  또다시 이런 논쟁이 일어난 것이 흥미로웠고, 저는 로드존슨이 키노트에서 바로 이에 대한 반론을 하지 않을까 하는 기대도 했었습니다. 그런데 로드 존슨은 핵심가치 같은 큰 그림에서의 스프링이 추구하는 바를 이야기했고, 이 논쟁에 대한 직접적인이야기는 하지 않았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;바로 셋째날에 한 이 발표 &quot;Spring and Java EE 6,Synergyor Competition?&quot;에서 유겐할러가 그에 대한 구체적인 대답을 했습니다.  유겐할러는 최근 이런 논쟁들을 보고서 발표제목을 바꾸었다고 했고  이미 발표 몇시간 전  팀블로그에 아래와 같은 글을 올려서 이날 발표에서 말할 내용을 먼저 공개하기도했습니다. 저는 컨퍼런스가 끝난 후에야 이 글이 올라온 것을 보았는데, 발표에서 가장 뒷부분에 강조한 핵심내용이 정리되어있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.springsource.com/2010/10/19/spring-3-on-a-java-ee-6-server/&quot; class=&quot;bare&quot;&gt;http://blog.springsource.com/2010/10/19/spring-3-on-a-java-ee-6-server/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;뒤에서 언급되겠지만, 간단히 정리하면  JavaEE6 환경에서도 스프링을 사용하면  플랫폼에서 제공되는 기술들을 일관된 프로그래밍과 설정 모델로 조화시키고, 더 많은 기술과 환경을 조합할 수 있으면서, Java EE7보다 더 빨리 발전하는 기능들을사용할 수 있는 것이라는 주장이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;사실 유겐할러는 이 날 발표와 비슷한 주제의 발표를 Java EE6 스펙의 확정 전에도 여러 번 한적이 있었습니다. 작년에 한 발표도 아래 링크에서 동영상으로 올라와 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.infoq.com/presentations/Spring-and-Java-EE-6-Jurgen-Holler&quot; class=&quot;bare&quot;&gt;http://www.infoq.com/presentations/Spring-and-Java-EE-6-Jurgen-Holler&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;확정된 스펙을 가지고 이야기 한 것이 작년에 한 위의 발표와 달랐던 점이였습니다. 발표 중에 유겐할러는 Spring 쪽에서도오랫동안 EE 6 스펙의 확정을 기다렸다고 했고 EE6 확정 직후에 발표된 Spring 3.0에서도 EE6의 스펙을 많이지원하고 있음을 강조했습니다.  발표 제목 중에  포함된 &quot;Synergy or Competition&quot;중,  &quot;Synergy&quot;를주장하고 싶었던 것이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스프링의_역할&quot;&gt;스프링의 역할&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 것은 이번 컨퍼런스 내내 키노트와 여러 세션에서 반복되어 언급된 말이였는데, 이 발표에서도 스프링을 사용하면 Java EE와 동등한 기능을쓰면서도 애플리케이션을 어느 서버에서도 배포될 수 있다는 것을 강조했습니다.  대상 플랫폼의 런타임에 적용되어서, 서비스추상화를 통해 공통적인 Programming model과 설정으로 개발을 할 수 있고,  전통적인  애플리케이션 서버 역할의 대안을 제공한다고 했습니다. Java EE 5의 스펙 중에서도 @PostConstruct, @PreDestoy ,@PersistenceContext, @PersistenceUnit , @Resource, @EJB, @TransationAttribute 등의 많은 Annotation을 지원하는데, 원하는 Semantics에 따라서 조합하고조화시켜(Mix &amp;amp; Match) 사용하라고 권장했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;java_ee6_스펙_리뷰와_스프링의_지원&quot;&gt;Java EE6 스펙 리뷰와 스프링의 지원&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;java_ee_프로파일&quot;&gt;Java EE 프로파일&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JavaEE프로파일은 EE 스펙 중 일부를 일부를 묶어서 구분하는 개념이였습니다. 가령 &quot;Web Profile&quot;이라고 하면 EE 스펙 중웹기술에 해당하는 Servlet, JSP, jSTL, JTA, JPA, EJB 3.1 Lite를 포함하는 것이라고 했습니다.그래서 벤더들이 초점을 두고 싶은 프로파일만 구현하고, 이미 지나간 잘 안 쓰이는 스펙에 대해서는 구현할 필요가 없이 그 분야의 프로파일에 대해서만 인증을 받을 수도 있다는 것입니다. 좋은 개념이기는 하나 현실적으로 웹프로파일만 구현하고 있는 벤더는아직까지는 없다고 말했습니다. 그런데 컨퍼런스가 끝난 뒤에 &lt;a href=&quot;http://mechsoft.com.tr/Mechsoft/software/en/acikkaynak/siwpas.html&quot;&gt;SIwpas&lt;/a&gt;라는 Web Profile만 구현한 서버를 우연찮게 발견하기도 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 스프링은 플랫폼에서 어떤 라이브러리가 제공되던지 상관 없이 사용할 수 있으므로  Java EE 프로파일은 스프링에 영향을 미치지는 않는 개념입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;servlet_3_0&quot;&gt;Servlet 3.0&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Servlet3.0 스펙에는  web.xml을 간결하게 해주는 &quot;framework auto deployment&quot;기능이 포함되고, 스프링도자동배포 기능 추가를 계획하고 있었습니다. 그리고 비동기적인 Http처리인 Comet은 Spring MVC에서 특별한request/reponse 형식으로 지원할 것이라고 했습니다. 당연히 이전 서블릿 스펙을 지원하는 서버에서의 하위호환성도유지할 것이라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jsf_2_0&quot;&gt;JSF 2.0&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JSF 2.0에는 UI 컴퍼넌트 부분에서 Ajax 지원와 페이지 선언 언어 등이 개선됩니다.. 그리고 스프링의 @Component와 @Value와 유사한 역할인@ManagedBean, @ManagedProperty 애너테이션이 있습니다. 스프링에서는 현재의 JSF지원 기능을 새 버전에맞추어 이어나가면서 3.1버전에서는 JSF 지원을 더 확장할 수 있도록 연구 중에 있다고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jpa_2_0&quot;&gt;JPA 2.0&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JPA2.0에는 쿼리 타임아웃, 표준화된 쿼리 힌트 등이 추가되었습니다. 역시 스프링에서는 현재의 JPA의 지원기능을 이어나가고,트랜잭션 관리에서 JPA 2.0의 모든 기능을 다 열어준다고 했습니다. 이미 Spring 3.0에서 Hibernate,EclipseLink, OpenJPA 등의 최신버전들과 이런 기능들을 쓸 수 있다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jsr_303_bean_validation&quot;&gt;JSR-303 Bean Validation&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JSF 2.0과 JPA 2.0에서 동시에 지원하는 스펙인데, Spring 3.0에는 SpringMVC의 Validation기능으로 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jax_rs&quot;&gt;JAX-RS&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JAX-RS((Java™API for RESTful Web Services)는 REST방식의 웹요청 처리를 지원하는 표준 API입니다. 이미Jersey같이 JAX-RS를 지원하는 프레임웍에서 Spring을 자연스럽게 같이 연결해서 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 코드에 @Path 아노테이션은 JAX-RS에서 정의된 것인데, 여기에 스프링의 &lt;code&gt;@Autowired&lt;/code&gt; 를 같이 쓰고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Path(&quot;widgets&quot;)
public class WidgetsResource {
    @Autowired
    private WidgetsService service;

    @GET  @Path(&quot;{id}&quot;)  @Produces(&quot;text/html&quot;)
    public String getWidget(@PathParam(&quot;id&quot;) int id){
     ...
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링 3.0에서도 Spring web MVC에서 나름대로의 스펙을 가진 REST지원 기능이 있습니다. 사실 위의 @Path와@PathParam 은 스프링의 @RequestMapping, @RequsetParams 아노테이션과 무척 유사해보이는, 비슷한프로그래밍 모델을 가지고 있습니다. 왜 스프링개발자들이 Spring MVC에서 JAX-RS를 바로 지원 안하고 나름대로의 스펙을만들었는지에 대해서는, 스프링소스의 팀 블로그를 통해서 밝힌 적이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.springsource.com/2009/03/08/rest-in-spring-3-mvc/&quot; class=&quot;bare&quot;&gt;http://blog.springsource.com/2009/03/08/rest-in-spring-3-mvc/&lt;/a&gt; 참조&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기존의 JAX-RS 스펙을 바로 지원하는 것도 프로토타이핑해봤지만, 자연스럽게 않게 억지로 끼워 맞추는 듯한 방식이 나왔고, 결국Spring MVC사용자들에게 더 일관적이고 편한 방식을 제공하는 나름대로의 기능을 넣기로 결정했다는 것이였습니다. 결국JAX-RS와 Spring MVC는 REST 지원부분에서 겹쳐지는 부분이 생겼고, 이를 두고 스프링은 표준 스펙을 존중하지않는다는 비난을 하는 사람도 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이날 발표에서도 유겐할러는 Spring MVC는 근본적으로 MVC구조라서 View의 rendering을 하는 부분을 분리할 수 밖에 없고, 따라서JAX-RS 방식과 달라질 수 밖에 없다고 했습니다. 그리고 &lt;a href=&quot;https://jersey.dev.java.net/&quot;&gt;Jersey&lt;/a&gt;, &lt;a href=&quot;http://www.jboss.org/resteasy/&quot;&gt;RESTEasy&lt;/a&gt;, &lt;a href=&quot;http://www.restlet.org/&quot;&gt;Restlet&lt;/a&gt;와같은 JAX-RS 구현체를 쓴다고 해도 Spring를 같이 쓸 수 있으니, 상황에 따라서 Spring MVC의 REST 기능이나JAX-RS 구현체를 모두 골라서 쓸 수 있다고 했습니다. UI페이지와 REST요청을 같이 처리해야하는 애플리케이션에서는Spring MVC로,  계층적인 리소스 구조처럼 REST 방식을 깊이까지 쓰는 애플리케이션이라면 JAX-RS 구현체를 쓰는것처럼 말이죠. 스프링은 언제나 그래왔듯이 선택에 대한 것이라는 말을 덧붙였습니다. (Spring is (and alwayswas) about choice),&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 JAX-RS 스펙은 Java EE6에서 독립적인 스펙이고, 다른웹스펙과도 연관관계가 없고, JSF와 프로그래밍 모델도 다르다고 유겐할러는 설명했습니다.  스프링은 그런 관련성이 있는 스펙들을 일관성 있게 묶어가고 있다는 것을 대비시켜 보이기 위해서 굳이 그런 언급을 한 것이 아닐까하는 생각도 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;ejb_3_1&quot;&gt;EJB 3.1&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;EJB3.1는 EJB 3.0에 singleton bean과 비동기 메서드 호출, JNDI 이름에 대한 Convention 제공 등의기능이 추가된 것입니다. 그리고 Local session Bean과 Singleton Bean만을 가지는 &quot;EJB 3.1Lite&quot;라는 것도 정의했습니다. 대부분의 서비스 객체가 원격호출이나 Object pooling이 없이 쓰이는 스프링의 방식과유사한 것인데, 과거의 그런 기능들이 대부분의 상황에서 오버엔지니어링 이였음을 다시 한번 인정하는 스펙 추가가 아닌가 하는생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;흥미로운 스펙은 컨테이너가 Lock 관리를 해준다는 것인데,(container-managed locking) 아래 코드에서 @Lock 아노테이션이 그런 역할을 하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Singleton @Startup
@DependsOn({&quot;OhterBean1&quot;,&quot;OtherBean2&quot;})
public class SharedService {
    private Data sharedData;
    @Lock(READ)
    public String returnSharedDataValue(){
        return this.sharedData;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 @Singleton이 붙은 클래스의 모든 메서드는 기본적으로 쓰기 잠금이 걸린다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;유겐할러는 이런 스펙이 불필요할 상황이 많을 것이라고 했는데, ConcurrentHashMap 같이 thread-safe를 감안한자료구조를 선택할 수도 있고, synchronized나 volatile와 같은 키워드를 이용해서도 개발자가 그런 것들을 제어할수도 있다고 했습니다. 아뭏든 이런 Lock에 대한 디폴트 값을 제외하고는 @Singleton으로 설정되는 Bean은Spring이 관리하는 Bean과 상당히 유사해졌다고 말했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;EJB가 컨테이너와 스프링과의 관계는 EJB3.1에서도 변하지 않는다고 했습니다. EJB 스펙은 나름대로의 Container에 의해서 지원되는 것이고, 필요하다면Spring에서 이를 접근할 수도 있는 것이죠. 그리고 EJB 3.1의 비동기 호출 스펙인 @Asynchronous은@Aysnc 로 Spring에  반영되어, 영향을 주었다고 했습니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jsr_299_web_beans_cdicontexts_and_dependency_injection&quot;&gt;JSR-299 Web Beans - CDI(Contexts and Dependency Injection)&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Webbeans&quot;라는 이름으로 불렸던, 공식적으로는 &quot;Contexts and Dependency injection&quot;이라는 명칭으로붙여진 이 스펙과 스프링을 눌러싼 논쟁들은 표준 제정 과정 당시에도 가장 뜨거운 화제였었고, 이 날 발표에서도 개인적으로 가장관심이 가는 부분이였습니다.  이 스펙은 원래 JSF에서 Bean관리를  개선해서 JSF와 EJB를 잘 연결하는 역할을 위해만들어졌지만, 점점 확정된 스펙으로 발전해 나갔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CDI에서는  Type-safe DepedencyInjection, Interceptor, 이벤트 통지, Web conversation context 등의 풍부한Dependency Injection모델을 Annotation을 통해서 제공합니다.  javax.decorator,javax.context, javax.inject, javax.event 등에 다양한 패키지에 나눠서 들어가 있고, 이미@Resource 등의 아노테이션이 있는 JSR-250이 담당하는 패키지인 javax.annotation,javax.interceptor 에도 추가되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;발표 슬라이드에는 나와있지 않지만,여기서 유겐할러는 CDI의 부정적인 면들도 언급을 합니다. EJB 3.1이 이 Component 모델을 뒤에서 떠받치는 역할을하지만, EJB와 CDI는 각각의 나름대로의 역할과 생명주기를 가지고 있는 그렇게 다른 Semantics가 섞이면 혼란을 불러일으킬 수 있다고 합니다. 그런 혼란에서 오는 어려움은 그 날 발표 슬라이드나 어떤 슬라이드 내에서는 표현될 수 없고, 실제로개발을 해보고 디버깅을 해서 겪어봐야지 알 수 있다고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 이 스펙은  스프링의 프로그래밍 모델과겹쳐지는 부분이 있고, Spring 3.1에서는 스프링의 원래 프로그래밍 모델을 더 발전시켜서 JSR-299과 표현력과 기능을능가하겠다고 했습니다. 이미 Spring에서도 다양한 scope의 빈을 정의할 수도 있고, Stateful한 웹애플리케이션을개발할 수 있다는 Spring Web Flow가 따로 프로젝트로 나와있지만, Spring 3.1에서 추가될Conversation Management는 그런 것들을 더욱 일반화 시켜서 Spring Core 쪽으로 끌어올리겠다는 의미로해석됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spring_on_java_ee6&quot;&gt;Spring on Java EE6&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;앞에서 언급했듯이, 유겐할러는 발표의 뒷부분에서 최근 논쟁에 대한 대답들을 정리해서  설명해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첫째,  Java EE6 서버는 스프링이 참조하는 미들웨어를 제공하는 좋은 실행환경이라는 것입니다. Java EE6 서버에서제공하는 Servlet 3.0, JSF 2.0, JPA 2.0 등의 플랫폼 기술들은 스프링은 소비자로서 사용할 수 있다고했습니다. 그리고 다소 Java EE 스펙과 중복이 될 수 있는 부분인 EJB나 CDI관련 부분은 Java EE6 기능의일부분에 불과하다는 것이였습니다. GlassFish에서는 코드량 기준으로 5% 정도만 차지할 뿐이고, 아마 5%가 넘는 다른기능들도 사용하지 않고 있는 것이 많을 것이라고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;둘째, 스프링을 사용하면 필요에 따라더 넓은 기술을 선택할 수 있기 때문에 그것이 Java EE6 서버를 사용하는 자연스러운 방식이라느 것입니다.  JSF대신 Wicket이나 GWT,  EE clustering대신 Coherence 등을  쓸 수 있고, , 스프링의 jar파일은 4MB바이트 정도로 크지 않아서 이 용량이 문제가 되는 경우는 거의 없다고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;셋째,Java EE6 서버 중 GlassFish만이 지금 GA(Generally Available)버전 이상인 상태이고, JBoss나WebSphere는 아직 안정화된 버전이전이기 때문에 Java EE 6서버만을 선택한다면 실행환경에 제약이 있다고 했습니다.스프링은 Tomcat 5,6,7버전, Java EE5 서버, 그리고 Google App Engine 같은 것에서도 돌아갈 수있고, Java EE5를 쓰면서도 스프링을 사용하면 Hibernate 3.6 같은 구현체를 써서 EE6 스펙을 쓰는 선택도가능하다고 했습니다. 다양한 플랫폼 환경에서 스프링이 조율역할을 한다는 것입니다. 실제로 최근에 올라온 &lt;a href=&quot;http://www.dzone.com/links/r/cdi_a_major_risk_factor_in_java_ee_6.html&quot;&gt;CDI - A Major Risk Factor in Java EE 6&lt;/a&gt;라는 글에서는 아직 안정화되지 않은 버전의 서버에서 CDI를 적용하다 어려움을 겪은 이야기가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;넷째,  EE6는 2009년 초 기술환경에 맞춘 것이고, 그 후로 지금까지 분산캐시, Cloud 등 많은 기술들이 중요하게떠올랐는데,  그런 기술들을  스프링을 통해서 서버를 업그레이드할 필요없이 훨씬 빠른 시기에 지원받을 수 있다는 것입니다. 이번컨퍼런스 내내 NoSQL, 분산 캐시, Social network 같은 다양한 주제들이 강조된 것도 그런 강점을 강조하기 위한전략으로 보였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정리하자면 Spring과 Java EE6는 잘 맞는 궁합이면서 중복되는역할의 라이브러리 용량은 현실적으로 문제될 것이 없으며, 스프링으로 더 다양한 기술과 실행환경을 활용하면서 일관된 프로그래밍모델로 개발할 수 있다는 것이 핵심 주장이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>PMD ruleset 검토 의견 사례</title>
      <link>https://blog.benelog.net//2702876.html</link>
      <pubDate>Thu, 28 Oct 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2702876.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이전에 PMD 룰셋 검토했을 때 정리했던  의견입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rule에 대한 유용성은 프로젝트의 상황에 따라서 많이 다르기 때문에, 아래 의견은 참고만 하셨으면 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2857%;&quot;&gt;
&lt;col style=&quot;width: 14.2858%;&quot;&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;Rule Set&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;Rule name&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;Message&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;분류&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;의견&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;설명, 의견 사유&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;strong&gt;참고자료&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Basic Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;EmptryInitializer&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;+&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;PMD 5.0에서 추가될 룰로 Maven plugin에서는 지원되지 않음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Braces Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;IfStmtsMustUseBraces&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Avoid using if statements without curly braces&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;if문뒤의 한줄짜리 처리문도 \{}가 없으면 경고를 줌. 가독성을 높여주므로 포함시키는 것이 바람직.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Code Size Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;TooManyMethods&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;This class has too many methods, consider refactoring it.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;한 클래스에 지나치게 많은 메서드가 포함되면 나는 경고. 적당한 클래스 크기를 정하는데 도움을 주나, 정말 논리적 연관성이 있는 작업을 많이 담고 있는 클래스도 존재할 수 있으므로 적정 숫자에 대해서는 논의 필요함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Controversial Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;OnlyOneReturnRule&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;A method should have only one exit point, and that should be the last statement in the method&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;메서드 중간의 return문은 복잡한 조건문의 구조를 단순하게 하는데 도움이 경우가 많고, 권장하는 사례도 있음.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;켄트백의 구현패턴 - 7장 중 &apos;보호절&apos;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Controversial Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;DataflowAnomalyAnalysis&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Found &apos;DD&apos;-anomaly for variable &amp;#8230;&amp;#8203;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Info&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;전체 데이터의 흐름을 분석해서 return값과 상관없는 변수에 값을 쓴다든지 하는 상식적이지 않은 경우를 잡아내줌. 캐시나 interceptor등 그런 흐름이 꼭 필요한 코드도 경고해줘서 지나치게 많은 경고를 생성함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Controversial Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Unused Modifer&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Avoid modifiers which are implied by the context&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;interface는 당연히 모든 method가 public이니 따로 선언해줄 필요가 없는 현상등을 주의를 줌. 개발자에 따라서는 public이 있는 것이 더 명시적이라고 생각할 수도 있고, 이미 모든 인터페이스가 public으로 다 선언되어 있는 상황에서는 개발자들에게는 무의미한 수정으로 인식될 위험도 있음.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Design Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;UnnecessaryLocalBeforeReturn&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Consider simply returning the value vs storing it in local variable ….&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;return 전에 따로 local 변수로 반환할 값을 선언할 때 주는 경고. 기능적으로는 별 의미 없는 코드이나, return 문장에는 @SupressWarning 의 Annotation을 추가할 수 없기 때문에, Annotation 적용 범위를 최소화하기 위해 그런 선언이 필요한 때도 있음. Debug Mode에서도 값의 추적에 약간의 편리성이 생길 수 있음.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java Language Spec 9.7, Effective Java 2ed - Item 24&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Design Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;UncommentedEmptyConstructor&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Document empty constructor&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;빈 생성자에 comment가 없어도 코드의 이해에 무리가 없는 경우가 많음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Design Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;UncommentedEmptyMethod&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Document empty method&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Listener류의 인터페이스의 구현에는 특정 메서드는 아무 일도 하지 않는 메서드로 남겨놓을 때도 있고, 종종 발생하는 상황임. 그리고 그 상황에 주석이 없다고 해도, 코드 이해에 무리가 없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Design Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ImmutableField&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Private field &apos;&amp;#8230;&amp;#8203;&apos; could be made final; it is only initialized in the declaration or constructor.
EmptyInitializer&lt;br&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;멤버변수 중 선언되어서 한번만 값이 대입된 변수는 final로 할 수 있다고 알려주는 경고. 개발자가 Final로 의도하지 않지만, 현재 기능에서는 한번만 대입하고 있는 상황도 존재할 수 있다고 생각됨.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Jakarta Commons Logging Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ProperLog&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Logger should be defined private static final and have the correct class&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;+&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;private static final Log log = LogFactory.getLog(해당클래스) 로 로거를 선언할 것을 알려줌. 대체로 무난히 적용가능하나, 프로젝트의 로그 정책이 특별한 경우도 있으므로 논의해볼만 함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java Bean Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;BeanMemberShouldSerialize&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Found non-transient, non-static member. Please mark as transient or provide accessors.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java Bean의 명세를 검사하는 규칙. 일반적인 클래스는 transient하지 않으면서도 acessor가 없는 멥버 변수가 올 수 있는 경우가 많음. 스프링에서 선언하는 bean들은 setter만 가지는 경우가 많으므로 대부분 이 규칙에 어긋나게 됨. (스프링에서 bean은 java bean명세보다 보다 넓은 bean을 의미하는 것으로 생각하면 됨)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Java Loggins Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Systemprintln&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;System.out.print is used&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Error&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;웹Application에서는 반드시 피해야할 코드라서 출시시에는 꼭 걸러내야하나, 분류가 Error가 되어 있는 것에 대한 논의는 필요함. 그리고 Console에서 도는 간단한 프로그램이나 테스트코드에서는 System.out이 들어가는 것이 결함이 아니므로 폴더 범위나 네이밍룰을 추가한 보다 정교한 Rule지정하는 것이 바람직함(예를 들어 Controller, Service, DAO안에는 System.out불가)&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Junit Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JunitAssertionsShouldIncludeMessage&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JUnit assertions should include a message&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;테스트 결과를 확인할 때, 메시지가 포함된 것이 바람직하나, 테스트 코드 작성을 장려하기 위해 테스트 코드에 대한 제약은 줄이는 것이 좋을 것으로 판단됨&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Junit Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JunitTestsShouldIncludeAssert&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JUnit tests should include assert() or fail()&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;assert나 fail이 있는 테스트코드가 의미가 있으나, 그렇지 않은 코드도 없는 것 보다는 나으므로, 테스트 코드 작성을 장려하기 위해 관대한 정책을 권장함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Migration Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Junit4TestShouldUseTestAnnotation&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JUnit 4 tests that execute tests should use the @Test annotation&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Junit3.8로 기준으로 작성된 코드에서 Junit4의 요건을 요구하면서 warning을 냄.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Naming Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;LongVariable&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Avoid excessively long variable names like …&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;표현력이 높은 변수이름을 짓는 것을 권장하기 위해서 길이제한을 두는 것은 바람직하지 않음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Naming Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;ShortVariable&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Avoid variables with short names like…&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&quot;is&quot;,&quot;os&quot;등의 2글자 변수명일 때 나는 경고. 실제로 2글자만으로도 문맥상으로 충분한 표현력을 가지는 상황도 있고, 한 메서드가 크게 길어지지만 않는다면 local variable의 경우에는 다소 관대한 정책을 넣는 것도 무리는 없음&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Optimization Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;AvoidInstantiatingObjectsInLoops&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Avoid instantiating new objects inside loops&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Loop안에서 new keyword가 존재하면 나는 경고임. 피할 수 있는 경우라면 피해야 겠지만, GC시점이 약간이나마 늦어질 수 있고, collection.clear같은 청소 매서드 호출이 필요해진다거나, Thread safe해지지 않는 경우 등 손해가 있는 상황이 있으므로, 항상 지켜야할 지침이라고는 보기 어려움&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Optimization Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;LocalVariableCouldBeFinal&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Local variable &apos;&amp;#8230;&amp;#8203;&apos; could be declared final&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;제외요망&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;local 변수 중 final이 될 수 있는 것을 알려주는 정보. Final이 되면 경고를 주는 이와 상반된 룰도 존재함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Optimization Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;MethodArgumentCouldBeFinal&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Parameter &apos;&amp;#8230;&amp;#8203;&apos; is not assigned and could be declared final&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;방어적 프로그래밍을 위해서 파라미터는 final로 선언하는 것이 바람직하나, 모든 메서드를 그렇게 하면 메서드 시그니처가 전부 다 길어지기 단점이 있음. 기술인프라성 공통코드에만 적용하는 방안이 바람직하다고 생각함&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Strict Exception Rules&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SignatureDeclareThrowsException&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;A method/constructor shouldn&amp;#8217;t explicitly throw java.lang.Exception&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;메서드나 생성자가 throws Exception으로 최상위 Exception을 던질때 주는 경고. Checked Exception의 남발을 막고, 정교한 Exception선언을 돕는다는 장점도 있어서 되도록 권장하고 싶지만, 기존 코드의 Exception 선언이 throws Exception이 남용되어 있을 때 등 프로젝트 후반기에는 적용이 어려움&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;String and StringBuffer&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;AvoidDpolicateLiterals&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;The String literal &quot; rows, page &quot; appears 4 times in this file; the first occurrence is on line 70&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Warning&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;논의필요&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;중복 되는 문자열 상수가 많은 경우에 나타남. 내부적으로는 java의 상수풀을 사용하게 되므로 성능에는 영향은 없음. 반복되는 문자열도 따로 상수선언을 하게 하는, 좋은 코딩습관에 도움이 되나 ,테스트 클래스 등의 안내 메시지 등의 그리 중요하지 않은 부분에서 지나친 상수선언을 하게 할 수 있음.&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
	</description>
    </item>
    <item>
      <title>SpringOne2GX 2010 (2) Spring 3.0 &amp;#8594; 3.1 &amp;#8594; 3.2 Themes &amp;amp; Trends</title>
      <link>https://blog.benelog.net//2700328.html</link>
      <pubDate>Sat, 23 Oct 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2700328.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링 프레임웍의 핵심 개발자 유겐할러는 &quot;Spring 3.0 &amp;#8594; 3.1 &amp;#8594; 3.2 Themes &amp;amp; Trends&quot;라는 제목으로 두번째 날 첫시간에  발표를 했습니다. 로드존슨은 &lt;a href=&quot;http://www.yes24.com/24/goods/3691866&quot;&gt;어느 인터뷰&lt;/a&gt;에서 지금까지 만났던 가장 뛰어난 개발자는 생각할 것도 없이 유겐할러라고 말한 적이 있었고, 그렇게 전적인 신임을 받고 있어서 인지 유겐할러는 스프링 core 모듈의 대부분의 코드를 만들었다고 합니다. 그래서 유겐할러는 스프링 커뮤니티에서 로드존슨에 버금가는 유명인인데, 예상대로 이 날 발표 장소에는 사람들이 꽉 차게 몰려왔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;발표에 들어가기 전에, 스프링 3.0 버전을 쓰는 사람과, 그중 실제 3.0에 들어간 기능을 쓰는 사람들은 손을 들어봐라고 유겐할러가 이야기했는데, 절반 정도의 사람들이 손을 들었던 것 같습니다. 컨퍼런스에 와서 그 발표를 들은 사람을 대상으로 한 질문이라서 당연한 것일수도 있지만 3.0버전의 기능이 커뮤니티에서는 호응이 괜찮다는 느낌이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;유겐할러는 자신이 맡은 책임 중 핵심적인 것은 클라우드, 분산 캐시와 같은 최신의 경향(modern day trends)에 맞추어 스프링 Core에 어떤 것이 들어갈지를 결정하는 것이라고 했습니다. 뒤에서 설명된 3.1의 캐시 Abstraction, 3.2의 fork-join pool 지원 등이 그런 트렌드을 쫓아가기위한 대표적인 예였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3.1과 3.2는 3.0의 자연스러운 다음버전이라고 합니다. 큰 구조변화보다는 기능이 추가되는 성격의 버전이라는 의미라고 생각되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/spring-one/spring-3-0-to-3-2.jpg&quot; alt=&quot;spring-3-0-to-3-2.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;릴리즈_일정&quot;&gt;릴리즈 일정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;발표 중간 중간에 이야기한 향후 스프링버전의 릴리즈 일정은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;3.0.5 : 2010년 10월에, 곧. 3.05 이후로 바로 3.1은 위한 마일스톤 버전으로 넘어간다고 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.1 M1: 2010년 11월 말&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.1 M2: 2011년 1월 초?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.1 RC1 : 2011년 2월말?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.1 GA  : 2011년 3월말?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3.2 :  2011년 말 경&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;뒤로 갈수록 물음표(?)가 들어가 있고, 지금까지 일정을 정확히 지킨 적이 없었던 것으로 보아 실제로 저 일정에 나올 가능성은 적다고 예상됩니다. Spring 3.0도 2008년에 이야기할 때는 2009년 초에 나올 것이라고 말한 적이 있는데, 실제로 3.0.GA 버전이 릴리즈된 것은 2009년 12월이였습니다. (&lt;a href=&quot;http://toby.epril.com/?p=459&quot;&gt;스프링 이슈트래커로 본 Spring 3.0의 전망&lt;/a&gt;, &lt;a href=&quot;https://jira.springframework.org/browse/SPR#selectedTab=com.atlassian.jira.plugin.system.project%3Aversions-panel&quot;&gt;Spring Jira의 실제 release 날짜&lt;/a&gt; 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일정관리를 엄격하게 안 하나 하는 생각도 들었는데, 공개 API의 모음인 프레임웍이라는 특성 때문에 일정보다는 완성도를 중요시 할 수 밖에 없을 것도 같았습니다. 두번째 날의 밤에 &apos;Birds of a feather session&amp;#8217;라고, 컨퍼런스의 트랙별로 발표자나 핵심개발자들과 이야기를 할 수 있는 자리를 마련해줬는데, 거기서 내부적으로 어떤 개발방법론을 쓰는지에 대한 질문이 나왔었습니다. 거기에서 스크럼 같은 일반적인 애자일 프랙티스를 따르기는 하는데, 프레임웍은 배포 한 후에 수정을 하는 것이 어렵기 떄문에 원래의 스크럼 방식과는 잘 맞지 않는 부분도 있다는 말이 답변 중에 나왔었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a_review_3_0&quot;&gt;A review : 3.0&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3.0에 추가된 기능들을 되돌아 봤는데, 이미 널리 알려진 기능들이라서 자세히 옮기지는 않겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Annotated component model.(JSR 330, JSR 303, JavaConfig등)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rest Support&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Portlet 2.0 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스케줄과 태스크 실행 개선&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java EE6 지원&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(자세히 아시고 싶으신 분들은 &lt;a href=&quot;http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/new-in-3.html&quot; class=&quot;bare&quot;&gt;http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/new-in-3.html&lt;/a&gt; 를 참조하시기 바랍니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이중 JavaEE6에 대한 내용은 세번째 날 역시 유겐할러가 발표한 &apos;Spring and JavaEE 6&amp;#8217;라는 세션에서 자세하게 설명 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a_preview_3_1&quot;&gt;A preview : 3.1&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3.1에 추가되는 주요 테마들은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Environment profiles for bean&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java-based applicaion configuration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cache abstraction&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Conversation Managment&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Servlet 3.0, JSF 2.0, Groovy&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;environment_profiles_for_bean&quot;&gt;Environment profiles for bean&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 기능은 bean 설정들을 환경별로 묶는 것을 지원합니다. 즉, 개발환경, 테스트환경, 운영환경에 맞는 bean 선언들을 그룹화해서 지정할 수 있게 하는 것이죠.
Bean 선언이나 Annotation선언에 Profile을 이름을 지정하는 방식이 될 것이라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;XML선언에는 다음과 같이 profile속성이 추가됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;beans .... profile=&quot;dev&quot;&amp;gt;

  &amp;lt;bean&amp;gt;...&amp;lt;/bean&amp;gt;

  &amp;lt;bean&amp;gt;...&amp;lt;/bean&amp;gt;

&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Annotation으로는 @Profile 을 지정하게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Service

@Profile(&quot;dev&quot;)

class UserService\{

.....

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Inject 가능한 API 형식으로 환경에 대한 추상화도 지원할 것이라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보통 database의 URL 같은 속성들은 별도의 property 파일로 빼서 관리하는 placeholders라는 기능을 쓰는데, 이 기능도 실제 환경에 의존적으로 placeholder를 나름대로 지정할 수 있는 기능이 나온다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;java_based_applicaion_configuration&quot;&gt;Java-based applicaion configuration&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 3.0에 JavaConfig 기능이 포함되어서 @Configuration, @Bean 등의 Annotation이 추가되었는데, 어떤 기능들이 더 추가되는 것일지 궁금했었습니다. 3.1에 들어가는 Java-based configuration은 tx나, aop 같은 custom name-space와 같은 선언들이 Java파일로도 가능하게 하는 것이였습니다. Spring Batch나 Security, Integration에서도 이러한 custom name-space들을 많이 쓰는데, 그런 설정들이 xml로 bean 선언을 할 때는 설정을 간편하게 하지만, JavaConfig로 옮기기에는 불편해서 아쉬웠던 점이 있습니다. Spring Batch에서 ItemReader, ItemWriter 선언을 JavaConfig로 옮겨보니 훨씬 코드가 짧아지고 Compile time의 validation 범위도 넓어져서 만족했었는데, Job과 Step의 선언은 custom namespace로 하는 것이 더 간편해서, XML에 남겨두었었습니다. 그리고 유사하게&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;http://www.amazon.com/Enterprise-Integration-Patterns-Designing-Deploying/dp/0321200683/ref=sr_1_1?ie=UTF8&amp;amp;qid=1287800130&amp;amp;sr=8-1[Enterpise Interation Patterns]를 구현한 Apache Camel는 Java로 된 DSL을 지원하는데 반해서 Spring Integration은 간결한 선정을 하려면 Xml의 Custom name space를 사용해야 되어서 아쉬웠던 적이 있었습니다.(http://java.dzone.com/articles/spring-integration-and-apache 참조)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3.1에서 그렇게 javaConfig로도 추상화정도를 높인 선언을 할 수 있는 기능이 추가된다면, 스프링포트 폴리오의 다른  프로젝트에서도 많은 개선이 이루어질 것이라고 기대됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;cache_abstraction&quot;&gt;Cache Abstraction&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재도 스프링에 org.springframework.cache라는 패키지는 존재합니다. 그런데 아직까지는 EhCache에 대한 지원클래스만 있습니다. 3.1에서는 이 패키지를 채워넣을 것이고, 분산캐시를 기술과 연결되는 구현체도 포함될 것이라고 합니다. 기본적으로 EhCache, GemFire, Coherence를 지원한다고 밝혔습니다. 물론 인터페이스에 맞춰서 직접 구현하는 것도 가능하고, ConcurrentHashMap을 이용한 간단한 기본 구현체도 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;발표가 끝난 뒤에 저와 같이 간 일행인 김훈민 대리가 직접 유겐할러에게 찾아가서 Memcached에 대한 지원 계획을 물어봤는데, out-of-box로 바로 쓸 수 있는 Adaptor는 아직까지는 계획에 없다고 했습니다. 여러 가지 라이선스 문제 등에 부딪힐 수 있어서 협의가 필요한데, 원한다면 이슈 등록을 하고 투표를 하라고 이야기했습니다.  개인적으로 Simple Spring Memecached( &lt;a href=&quot;http://code.google.com/p/simple-spring-memcached/&quot; class=&quot;bare&quot;&gt;http://code.google.com/p/simple-spring-memcached/&lt;/a&gt;) 프로젝트와 유사한 Memcached 지원이 Spring Core에 들어갔으면 했는데, 다소 아쉬운 부분이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;많은 곳에서 이미 Annotation과 AOP를 활용해서 Cache를 활용해서 이미 예상을 했었는데, Spring 3.1에는 아래와 같이 Cache 지원을 위한 `@Cacheable`, `@CacheEvict` 라는 Annotation이 추가됩니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Cacheable
public Owner loadOwner(int id) \{

....

}

@Cacheable(condition=&quot;name.length&amp;lt;10&quot;)
public Owner loadOwner(String name)\{

....

}

@CacheEvict
public void deleteOwner(int id)\{

....

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Annotation이 붙은 메서드의 signature와 파라미터로 캐시에 넣을 key로 인식하게 됩니다. 유겐할러는 이런 방식의 처리가 Cache의 전형적인 사용의 80% 유형정도를 차지할 것이라고 말했습니다. Cache Abstraction은 Cache의 모든 기능을 통합해서 같은 API로 묶는 것이라기보다는, 주요 쓰임새를 더 짧은 코드로 편하게 쓸 수 있게 하는데 초점이 맞춰진 것으로 보입니다. 정교한 캐시처리나 각각의 캐시 특성에 맞는 API사용 등은 직접 특정 Cache의 API를 당연히 사용해야겠죠.
그리고 transaction처리에 쓰는 PlatformTransactionManager와 비슷하게 CachedManager라는 SPI가 들어가고, tx namespace처럼 cache에 대한 namespace도 추가된다고 합니다. &amp;lt;cache:annotation-driven /&amp;gt;과 같은 선언은 기존에 스프링을 쓰던 사람에게는 익숙하게 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 EhCache를 Spring에서 활요할 때 쓰는 @Cacheable annotation이 있는데, 이 모델이 좀 더 확장될 것으로 보입니다. 아래 자료에 현재에도 사용가능한 방식이 나와 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable&quot; class=&quot;bare&quot;&gt;http://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable&lt;/a&gt;http://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.tistory.com/1256&quot; class=&quot;bare&quot;&gt;http://whiteship.tistory.com/1256&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;converstion_management&quot;&gt;Converstion management&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Conversation session에 대한 추상화 계층을 추가될 예정이라고 합니다. 기본적으로 HttpSession을 포함하여 더욱 유연한 생존주기와 저장소를 제공한다고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보통 웹애플리케이션에서 여러 페이지간의 상태를 공유해야하는 Conversation 범위가 생길 수가 있는데, 보통 Session을 쓰는 것이 일반적이지만, 같은 윈도우에 있는 다른 탭이라도 같은 Session의 id가 먹는 상황이 있고, 수동적으로 이런 경계를 관리해야 할 때가 있습니다. 새로운 기능은 이런 것들을 위임할 수 있는 공통적인 기반을 제공한다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 기능은 Spring Webflow의 3.0에도 바탕이 될 것이고 MVC나 JSF에도 공통적으로 쓰일 수 있다고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 웹애플리케이션 뿐만이 아니라 메시징 환경에서 메시지 헤더 안에 conversation id를 포함시키는 쓰임새에도 적용가능하도록  Conversation을 위한 인터페이스는 범용적인 목적의 API가 될 것이라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;support_for_servlet_3_0&quot;&gt;Support for Servlet 3.0&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Tomcat 7, GlassFish 3 등에서 Servlet 3.0 스펙을 지원하는 Container에 대한 지원이 포함됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;web.xml에 명시적으로 프레임웍에 대한 Listener 선언을 하지 않고도  자동으로 deployment되는 옵션을 지원하고, 표준적인 파일업로드에 대한 지원이 된다고 합니다. 스프링의 MultipartResolver interface도 그 안에 포함될 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;enchance_groovy_support&quot;&gt;Enchance Groovy Support&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;lang:groovy&amp;gt;&lt;/code&gt; 로 xml 파일안에 Groovy를 쓸 수 있는 지원이 강화됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;base script classes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;custom bidings&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;스프링 빈들을 이름으로 암묵적으로 접근&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Velocity나 Freemarker 대신 쓰일 수 있는 Groovy 바탕의 Template 파일. Email 템플릿 같은 것에 사용할 수 있다고 하네요&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;cnamespace&quot;&gt;&lt;code&gt;c:namespace&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;XML로 Bean 선언을 할 때 p namespace과 같은 역할을 하는 c namespace가 추가됩니다. p가 &amp;lt;property name.. &amp;gt; 에 대한 짧은 표현였다면, c는 &amp;lt;constructor-arg&amp;gt;를 간결하게 표현할 수 있게 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래와 코드와 같이 거의 p namespace와 사용법이 똑같아 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;bean class=&quot;...&quot; c:age=&quot;10/&amp;gt;

&amp;lt;bean class=&quot;...&quot; c:family-ref=&quot;myFamily/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a_sneak_preview_3_2&quot;&gt;A sneak preview : 3.2&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;3.2에는 JDK 7을 바탕으로 추가될 수 있는 기능들을 계획하고 있었습니다. JDK 7은 2011년 7월에 release 예정이라서 3.2에 대한 Release도 당연히 그 뒤가 되겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 당연히 java 5, 6 user를 위한 기능도 있을 것이고, 그런 기능들은 Spring 3.1이 GA로 간 다음에 사용자들의 요청에 따라서 결정될 것이라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;java_se_7_support&quot;&gt;Java SE 7 Support&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring 3.2의 초점은 JRE 7 을 가장 잘 쓰는 사용법이라고 했습니다. JDBC 4.1에 대한 지원과 Java concurrent 패키지에서 개선되는 fork-join 프레임웍에 대한 지원계획이 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;멀티코어의_concurrent_프로그래밍에_초점&quot;&gt;멀티코어의 Concurrent 프로그래밍에 초점&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;동시 요청보다 Core수가 더 많은 시나리오에 초점을 맞추어서 API제공을 계획하고 있다고 합니다. 예를 들면 Spring Batch에서 큰 Xml파일을 처리할 때와 같이, 처리 요청은 하나이기 때문에 요청 건별로 병렬처리를 하기가 힘들지만 자원소모가 커서 병렬처리를 했을 때의 이득이 큰 경우를 염두에 둔 것이였습니다. 그런 곳에 사용할 수 있는 특화된 ForkJoinPool이 Application context안에 들어갈 것이라고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>SpringOne2GX 2010 (1) 키노트</title>
      <link>https://blog.benelog.net//2698818.html</link>
      <pubDate>Wed, 20 Oct 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2698818.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2010년 10월 19일부터 22일까지 시카고에서 열리는 SpringOne2GX 2010 행사에 참석하고 있는 중입니다. 정리가 완벽하게 되지 않아도 중간중간 듣고 본 것들을 올리도록 하겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;수정이력&quot;&gt;수정이력&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;(최초작성) 키노트의 내용을 발표 때 사용한 슬라이드 자료를 보면서 정리를 하려고 마음을 먹고 있었습니다. 그래서 특별히 메모도 하지 않고 있었는데, 키노트가 끝난 다음에서야 발표 자료가 아직 올라오지 않았다는 것을 알았습니다. 그래서 대략적으로 기억나는 것들을 같이 간 분들에게 물어보고 보완하면서 트위터에 올라온 글들 ( &lt;a href=&quot;http://twitter.com/#!/search/s2gx&quot; class=&quot;bare&quot;&gt;http://twitter.com/#!/search/s2gx&lt;/a&gt; )을 보고 회상한 내용으로  일단 정리합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;키노트의_제목은&quot;&gt;키노트의 제목은?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로드존슨은 첫장에 키노트의 제목을 표시하지 않고 &apos;여기에 제목이 들어간다&amp;#8217;는 슬라이드 템플릿에 있을법한 문구를 그대로 보여줍니다. 나름대로 웃음을 주려고 했던 의도도 있었지만, 지난 12개월동안 스프링 세계에 어떤 일이 일어났는지를 키노트에서 정리해야 하는데, 이번에는 그것이 쉽지 않았음을 이야기 합니다. 발표가 한참 지나서야 제목이라고 할만한 내용들을 이야기하는데, 간단히 정리하면 &apos;스프링의 성공요인이였던 Portability, Productivity, Innovation을  다음 10년에도 새로운 영역에서 이어서 가고, 개발자들이 더욱 핵심 비지니스 가치의 전달에 전념할 수 있도록 돕는다&amp;#8217;라고 말할 수 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;그동안_스프링_세계에_있었던_일들&quot;&gt;그동안 스프링 세계에 있었던 일들..&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;키노트 발표는 이번 컨퍼런스의 주요 후원자인 Accenture, Google, Saleforce.com에 대한 스폰서들에 대한 감사의 이야기로 시작되었습니다. 이번 컨퍼런스를 통해 비니지스 애플리케이션을 전통적인 데이터 센터에서 클라우드 환경으로 올리겠다는 비전을 강조할 것이라고 미리 예상을 했었고,  인터넷 업계, 시스템통합, SaaS 업계의 최강자들인 이들 세 업체가 그런 비전을 공유하고 후원업체로 전면에 나와 있다는 생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이어서 얼마전에 발표되었던 vFabric cloud platform의 그림을 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;참조 : &lt;a href=&quot;http://blog.springsource.com/2010/08/31/cloud-platform/&quot; class=&quot;bare&quot;&gt;http://blog.springsource.com/2010/08/31/cloud-platform/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 아키텍처에서 모니터링은 Hyperic, 메시징큐는 RabbitMQ, 캐슁을 위한 데이터그리드는 Gemfire가 들어가는데, 작년부터 스프링소스가 인수합병을 통해서 확보한 솔루션들입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이어서 이런 솔루션들이 현재 얼마나 중요한 시스템에서 쓰이고 있는지 레퍼런스를 알려줍니다. Gemfire는 NASA나 펜타곤에서, RabbitMQ는 인도의 수많은 인구들의 주민등록 정보를 관리하는 곳에 들어가 있다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 다른 스프링 포트폴리오 프로젝트의 대표적인 발전도 정리해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring 3.1 : Cache abstraction, 환경에 특화된 bean 설정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring Integration 2.0 : Spring Tools Suite를 통한 plugin지원, 더 많은 Adaptor 지원&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring Web flow 3.0 : Java flow Definition 지원 (&lt;a href=&quot;https://jira.springsource.org/browse/SWF-295&quot; class=&quot;bare&quot;&gt;https://jira.springsource.org/browse/SWF-295&lt;/a&gt; 이 이슈를 말한 것으로 짐작됩니다.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Grails : Spring Tools Suite에 추가된 Grails 지원 기능을 Demo로 보여주었습니다. 프로젝트 생성,  Entity 생성, Grails command 입력등을 Eclipse 안에서 편하게 할 수 있고, Grails View에서 Grails에 구조에 맞는 디렉터리 구성을 더 편하게 볼 수 있었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개인적으로 Cache abstraction에 가장 관심이 가는데, 이 주제를 다룰 내일 유겐할러의 발표를 기대해 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;스프링의_지난_10년과_그_다음&quot;&gt;스프링의 지난 10년과 그 다음&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로드존슨은 스프링 프레임웍이 이제 새로운 10년대를 맞이하고 있음을 강조합니다. 로드존슨이 직접 밝힌 스프링에서 가장 오래된 클래스는 &lt;a href=&quot;http://www.jarvana.com/jarvana/view/org/springframework/spring-web/2.5.6.SEC02/spring-web-2.5.6.SEC02-sources.jar%21/org/springframework/web/context/support/RequestHandledEvent.java?format=ok&quot;&gt;RequstHandlerEvent.java&lt;/a&gt;로 2001년 1월 17일 처음 만들어졌습니다. (스프링의 오래된 클래스들에 대해서는 토비님이 쓰신 &lt;a href=&quot;http://toby.epril.com/?p=171&quot;&gt;스프링코드의 역사&lt;/a&gt; 글에서도 재밌는 정보들을 찾을 수 있습니다.) 로드 존슨의 큰 아들의 다음 생일이 10번째 생일인데, 걔가 스프링보다는 나이가 어리다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇게 10년을 지나오면서 스프링을 성공으로 이끌었던 핵심가치들은 Portability, Productivity, Innovation의 3가지 단어로 설명했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Jetty, Tomcat을 포함한 많은 Application Server들에서 Spring 애플리케이션이 돌아갈 수 있었던 Portability는 이제 다음 목표를 이제 Google App Engine, VMForce와 같은 클라우스 서버 영역으로까지 나아가고 있습니다.  그리고 Grails, Spring Roo, Spring Tools Suite를 이용한 생산성 향상도 계속되고 있습니다. Spring Roo에서는 GWT 지원, Database reverese-engineering 기능이 포함된 다는 것을 홍보했습니다. 바이트코드 삽입(Instrumentation)을 통한 Application Monitoring 도구인 Spring Insight를 운영환경에서 사용할 수 있도록 성능저하가 없는 버전을 준비하고 있다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링이 나아가는 새로운 영역들 중에 Social media 결합, NoSQL 저장소 지원, 모바일 환경의 다양한 Client 지원 기능등이 특히나 신선한 소식이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;새로운_영역들&quot;&gt;새로운 영역들&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;nosql&quot;&gt;NoSql&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로드존슨은 NoSQL을 Not only SQL이라고 풀어줬습니다. 전통적인 데이터 저장소였던 RDB도 나름대로의 영역을 지키겠지만, 이제는 RDB만으로는 해결할 수 없는 문제들이 많아 생겼다는 것입니다. 그렇다고 RDB를 배제하는 것이 아니라는 것을 Not  only SQL이라는 말로 강조했다고 느껴졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 GORM에서는 Redis를 지원하는 Addon을 넣었다는 소식을 이미 들은 적이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.springsource.org/node/2839&quot;&gt;Grails adds GORM for Redis&lt;/a&gt; 참조&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이날은 neo4j(&lt;a href=&quot;http://neo4j.org/)를&quot; class=&quot;bare&quot;&gt;http://neo4j.org/)를&lt;/a&gt; 이용한 GraphDB 지원에 대한 코드를 보여줬습니다.  Graph DB는 친구사이 관계 같은 것이 저장되는 social media같은 서비스에 적합한 구조인데, 스프링과 neo4j의 결합은 원래 neo4j의 API를 쓰지 않고 annotation으로 필드 간의 관계를 설정하는 것이였습니다. 이것도 내부적으로는 Aspect J를 이용해서 동작한다고 했습니다. Graph DB를 위한 annotation은 JPA annotation과 같이 쓰여서 Domain object에서 관계형 DB와 Graph DB에 연결되는 정보를 동시에 볼 수 있었습니다.  로드존슨은 이를 polyglot persistence, cross persistence라고 표현했습니다. 다수의 저장소를 활용할 때 Java Object가 그 연결정보의 구심점이 되는 모습이 간단한 코드에서 잘 보여졌습니다. Spring roo에서도 Neo4j를 위한 Addon이 들어갔다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;Spring-data 프로젝트(http://git.springsource.org/spring-data)에서는 Graph DB이외에도 key-value, document, column 저장소들을 위한 하위 프로젝트가 진행되고 있었습니다.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;social_media와_다양한_client의_시대&quot;&gt;Social Media와 다양한 client의 시대&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로드존슨은 근래의 기술환경이 Mobile 환경의 다양한 Client와 브라우저에 대처해야 한다는 것을 상기시킵니다. 그리고 Twitter와 Facebook과 같이 Social Media와 연결된 개발도 중요한 이슈입니다. 스프링에서도 이에 대비하고 있는데, Spring-mobile(&lt;a href=&quot;http://git.springsource.org/spring-mobile)과&quot; class=&quot;bare&quot;&gt;http://git.springsource.org/spring-mobile)과&lt;/a&gt; Spring-social(&lt;a href=&quot;http://git.springsource.org/spring-mobile%29%EA%B3%BC%20Spring-social%28http://git.springsource.org/spring-social&quot; class=&quot;bare&quot;&gt;http://git.springsource.org/spring-mobile%29%EA%B3%BC%20Spring-social%28http://git.springsource.org/spring-social&lt;/a&gt;http://git.springsource.org/spring-social[&lt;a href=&quot;http://git.springsource.org/spring-social&quot; class=&quot;bare&quot;&gt;http://git.springsource.org/spring-social&lt;/a&gt;] ) 프로젝트가 이와 관련되어 있습니다. 그리고 GreenHouse(&lt;a href=&quot;http://git.springsource.org/greenhouse&quot; class=&quot;bare&quot;&gt;http://git.springsource.org/greenhouse&lt;/a&gt;)  프로젝트가 이들을 활용한 실제 예제가 되는 프로젝트입니다. GreenHouse는 &lt;a href=&quot;https://greenhouse.springsource.org/&quot; class=&quot;bare&quot;&gt;https://greenhouse.springsource.org/&lt;/a&gt; URL로 들어가볼 수 있고, Spring 개발자들의 social network 기능을 할 수 있어 보였습니다. iPhone와 Android client가 있는데, iPhone client는 시뮬레이터를 통해서 데모를 보여줬습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;missing_link_code_to_cloud&quot;&gt;Missing link - Code to Cloud&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로드존슨은 지금까지 스프링이 많은 영역들을 지원해왔지만, 그 중에 빠진 연결점(Missing Link)가 있었고, 그 부분은 소스관리, 이슈관리, Contious Integration, 배포에 관한 영역이였다고 합니다. 개발자들의 각각의 도구를 설치한다고 많은 시간을 소모하고 있는데, 이제 그 영역도 Code to Cloud라는 통합솔루션을 제공한다고 합니다. 이 솔루션은 Git + Hudson + Bugzilra + Mylyn + STS을 엮은 것이였습니다. Tasktop이라는 업체와 제휴를 통해 이를 제공했고, 시연도 직접 Tasktop의 CEO가 나와서 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;관련기사 : &lt;a href=&quot;http://www.marketwire.com/press-release/VMware-Brings-the-Cloud-to-Developers-With-Code2Cloud-Application-Lifecycle-Tools-NYSE-VMW-1337583.htm&quot; class=&quot;bare&quot;&gt;http://www.marketwire.com/press-release/VMware-Brings-the-Cloud-to-Developers-With-Code2Cloud-Application-Lifecycle-Tools-NYSE-VMW-1337583.htm&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;소개와 데모 동영상: &lt;a href=&quot;http://tasktop.com/connectors/c2c.php&quot;&gt;http://tasktop.com/connectors/c2c.php&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring2gx 2010에서의 시연 동영상 (&lt;a href=&quot;http://plixi.com/p/51699717&quot;&gt;http://plixi.com/p/51699717&lt;/a&gt; )&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Eclipse에서 테스트 코드 작성을 편하게 하는 설정</title>
      <link>https://blog.benelog.net//2688165.html</link>
      <pubDate>Fri, 1 Oct 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2688165.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;static_import_설정&quot;&gt;Static Import 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Organize import (Ctrl + Shift + O) 해도 static import의 *가 풀리지 않도록 하는 설정.
Junit4 이후의 assert나, mock library, matcher 등을 사용할 때 편리하다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Windows-Preference-Orgnize import&lt;/code&gt; 의 &lt;code&gt;Number of static imports&amp;#8230;&amp;#8203;&lt;/code&gt; 를 1로&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/eclipse-test/organize-imports.jpg&quot; alt=&quot;organize-imports.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;test_메서드와_import에_대한_template_설정&quot;&gt;Test 메서드와 Import에 대한 Template 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Window-Preference-Java-Editors-Template&lt;/code&gt; 에 자주 쓰는 템플릿을 추가한다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각각의 항목의 의미는 다음과 같다&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;name : 축약해서 사용할 문자&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;context : 해당 템플릿을 사용하는 에디터 종류. 여기서는 Java코드를 입력하므로 &apos;Java&amp;#8217;로 선택한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pattern :  템플릿 내용&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;즉, Java에디터에서 &apos;name&apos; 으로 정한 문자열을 치고 Ctrl + Space를 누르면 &apos;pattern&amp;#8217;의 내용이 자동입력 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/eclipse-test/templates.jpg&quot; alt=&quot;templates.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;test_메서드_추가&quot;&gt;Test 메서드 추가&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트 메서드를 추가할 때 필요한 library import로 메서드 선언을 한꺼번에 해주는 템플릿이다.이름을 spec으로 지정해서 쓰고 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@${testType:newType(org.junit.Test)}

public void ${specDescription}() {
  // given ${cursor}

  // when
  // then
  ${staticImport:importStatic(&apos;org.junit.Assert.*&apos;, &apos;org.mockito.BDDMockito.*&apos;, &apos;org.hamcrest.CoreMatchers.*&apos;)}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;test_라이브러리_import&quot;&gt;Test 라이브러리 import&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 메서드 추가 template에서 해주는 일 중 import를 하는 부분만 수행해준다.
ti라는 이름으로 지정해서 쓰고 있다.
(&lt;a href=&quot;http://wiki.kwonnam.pe.kr/java/junit/staticimports&quot; class=&quot;bare&quot;&gt;http://wiki.kwonnam.pe.kr/java/junit/staticimports&lt;/a&gt; 에서 참조했습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;${is1:importStatic(&apos;org.hamcrest.CoreMatchers.*&apos;)}${is2:importStatic(&apos;org.junit.Assert.*&apos;)}${is5:importStatic(&apos;org.mockito.Mockito.*&apos;)}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;spring_test_지원_annotation_추가&quot;&gt;Spring test 지원 Annotation 추가&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Junit4에서 Spring의 Application context를 올리는 테스트를 할 때 필요한 Annotation과 import 선언을 추가해주는 Template이다. &apos;springtest&amp;#8217;라는 이름으로 지정해서 쓰고 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;${:import(&apos;org.junit.runner.RunWith&apos;,&apos;org.springframework.test.context.ContextConfiguration&apos;,&apos;org.springframework.test.context.junit4.SpringJUnit4ClassRunner&apos;)}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { &quot;/applicationContext.xml&quot;})&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;mockito_관련_템플릿_추가&quot;&gt;Mockito 관련 템플릿 추가&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JUnit에서 Mocito의 annotation을 편하게 쓸 수 있게 해주는 선언이다. &apos;mockrun&amp;#8217;이라는 이름으로 지정해서 쓰고 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;${:import(&apos;org.mockito.runners.MockitoJUnitRunner&apos;)}
@RunWith(MockitoJUnitRunner.class)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;참고자료&quot;&gt;참고자료&lt;/h4&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.rapaul.com/2009/08/09/bddmockito-eclipse/&quot; class=&quot;bare&quot;&gt;http://www.rapaul.com/2009/08/09/bddmockito-eclipse/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://toby.epril.com/?p=1126&quot; class=&quot;bare&quot;&gt;http://toby.epril.com/?p=1126&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://wiki.kwonnam.pe.kr/java/junit/staticimports&quot; class=&quot;bare&quot;&gt;http://wiki.kwonnam.pe.kr/java/junit/staticimports&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://help.eclipse.org/helios/index.jsp?topic=/org.eclipse.jdt.doc.user/concepts/concept-template-variables.htm&quot; class=&quot;bare&quot;&gt;http://help.eclipse.org/helios/index.jsp?topic=/org.eclipse.jdt.doc.user/concepts/concept-template-variables.htm&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;favorite_설정&quot;&gt;Favorite 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자주 쓰는 static import 등록할 수 있음. 여기에 등록하면 미리 해당 라이브러리를 static import하지 않아도 Conentnt assist(Ctrl + Space)에서 에서 나오게 됨&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Windows-Preference-&amp;#8230;&amp;#8203;&amp;#8230;&amp;#8203;- Favorites&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/eclipse-test/favorites.jpg&quot; alt=&quot;favorites.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Favorites에 등록을 추천하는 클래스
* &lt;code&gt;org.junit.Assert.&lt;strong&gt;&lt;/code&gt;
* &lt;code&gt;org.hamcrest.CoreMatchers.&lt;/strong&gt;&lt;/code&gt;
* &lt;code&gt;org.mockito.Mockito.&lt;strong&gt;&lt;/code&gt;
* &lt;code&gt;org.mockito.BDDMockito.&lt;/strong&gt;&lt;/code&gt;
* &lt;code&gt;org.mockto.Matchers.*&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;자동생성되는_메서드에_unsupportedoperationexception_던지기_설정&quot;&gt;자동생성되는 메서드에 UnsupportedOperationException 던지기 설정&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자동생성 되어서 아직 구현되지 않은 메서드를 test fail로 인식하게 함 ( &lt;a href=&quot;http://toby.epril.com/?p=706&quot; class=&quot;bare&quot;&gt;http://toby.epril.com/?p=706&lt;/a&gt; 참고)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Preference – Java – Code Style – Code Templates 안에 Code/Method Body에 아래 코드추가&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;throws new UnsupportedOperationException();&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/eclipse-test/unsupported-operation-exception.jpg&quot; alt=&quot;unsupported-operation-exception.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;테스트_코드_짤_때_자주_쓰는_단축키&quot;&gt;테스트 코드 짤 때 자주 쓰는 단축키&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;코드_생성&quot;&gt;코드 생성&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl+ 1&lt;/code&gt; : Quick fix&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + Shift + O&lt;/code&gt; : import 절에 없는 클래스를 추가하거나 정리&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + Shirt + M&lt;/code&gt; : import 추가. 클래스 import를 static import로 전환할 수 있음.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;테스트_실행&quot;&gt;테스트 실행&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Alt + Shift + X, T&lt;/code&gt; : JUnit으로 실행&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + F11&lt;/code&gt; : Run (JUnit 실행 가능)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;코드_사이를_이동&quot;&gt;코드 사이를 이동&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + J&lt;/code&gt; : 테스트 코드와 실제 코드 사이를 이동 (moreUnit이 설치되어 있을 때)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + Q&lt;/code&gt; : 가장 마지막에 편집한 코드가 있는 곳으로 돌아가기&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + T&lt;/code&gt; : 인터페이스에서 구현 클래스 찾을 때&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Ctrl + Shift + 위아래 방향키&lt;/code&gt; : method 단위로 커서 이동(method 하나만 test 실행할 때 사용 하기 좋음&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;리팩토링&quot;&gt;리팩토링&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Alt + Shift + R&lt;/code&gt; : 리팩토링 이름 바꾸기&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Alt + Shift + V&lt;/code&gt; : 리팩토링 – 이동&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Alt + Shift + M&lt;/code&gt; : 리팩토링 – 메서드 추출&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Alt + Shift + I&lt;/code&gt; : 리팩토링 - 메서드 인라이닝 (추출의 반대)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Alt + Shift+ L&lt;/code&gt; : 리팩토링 local 변수 추출&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;more_unit&quot;&gt;More Unit&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트 코드와 실제코드 사이를 왔다갔다 할 수 있게 하는 Eclipse plugin. TDD의 리듬 유지에 도움이 됨&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;update site : &lt;a href=&quot;http://moreunit.sourceforge.net/update-site/&quot; class=&quot;bare&quot;&gt;http://moreunit.sourceforge.net/update-site/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Maven을 쓰고 있다면 설치후 Window-Preference-More Unit에서 아래 설정을 추가하는 것이 좋다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Directory for testcases : &lt;code&gt;src/test/java&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test Suffixes : &lt;code&gt;Test&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/eclipse-test/more-unit.jpg&quot; alt=&quot;more-unit.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;eclemma&quot;&gt;Eclemma&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eclipse 내에서 Code Coverage 측정. &lt;a href=&quot;http://blog.benelog.net/2212119&quot; class=&quot;bare&quot;&gt;http://blog.benelog.net/2212119&lt;/a&gt; 참조&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;update site : &lt;a href=&quot;http://update.eclemma.org/&quot; class=&quot;bare&quot;&gt;http://update.eclemma.org/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;STS를 쓴다면  Dashboard-Extensions에서 선택해서 설치해도 됨&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>private 메서드를 어떻게 테스트해야 할까요?</title>
      <link>https://blog.benelog.net//2685835.html</link>
      <pubDate>Mon, 27 Sep 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2685835.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;private 메서드를 어떻게 테스트해야 할지에 대한 질문은 테스트 코드 작성에 대한 자주 나오는 질문 중에 하나입니다. 이에 대한 답변을 간단히 정리해봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;많은 경우에 private method는 public 메서드에서 extract method 되어서 나온 것이므로 public을 통해서 간접적으로 테스트를 하는 것이 자연스럽습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;그래도 private 영역만 따로 테스트를 해야지 더욱 다양한 테스트 케이스를 편하게 작성할 수 있다거나 디버깅이 쉬워진다면 설계를 개선하라는 신호로 해석할만합니다. private 메서드가 하는 일이 크다는 신호로 해석하고 별도의 클래스로 분리하거나, 하위 클래스에서 상속을 해서 대체할 수 있는 가능성을 고려해서 protected로 해두는 것도 고려해 볼만합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;그럼에도_불구하고_private_메서드를_테스트해야한다면&quot;&gt;그럼에도 불구하고 private 메서드를 테스트해야한다면&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설계 개선 등을 바로 할 수 없는 상황이라 당장 private 메서드를 테스트해야할때 쓸수 있는 방법을 이어서 소개합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;테스트만을 해당 메서드를 package private(default 접근자)나 protected로 바꾸어서 테스트해볼 수도 있습니다. 일반적으로 테스트를 위해서 production 코드의 접근 범위를 넓히는 것은 클래스의 노출 범위를 커지게 하므로 바람직하지 않을 수도 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reflection을 이용하면 강제적으로 private 메서드를 호출할 수 있습니다. 다만 이렇게 하면 메서드이름 부분이 String값으로 넘겨지게 되므로, compile time에 메서드명의 오타가 검증되지 못하고, refactoring으로 메서드명을 바꾸어도 자동으로 String으로 적힌 부분은 바뀌지 않는 단점이 있습니다. 부작용을 감수하고서라도 쓰겠다고 각오가 된 곳에 제한적으로 사용하기를 권장드립니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Reflection으로 private 메서드를 호출하는 방법들은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;1_powermock에서_whitebox_invokemethod_메서드_활용&quot;&gt;(1) PowerMock에서 Whitebox.invokeMethod(..) 메서드 활용&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://code.google.com/p/powermock/wiki/BypassEncapsulation&quot; class=&quot;bare&quot;&gt;http://code.google.com/p/powermock/wiki/BypassEncapsulation&lt;/a&gt;  참조&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;2_junit_addons에_포함된_privateaccessor&quot;&gt;(2) JUnit Addons에 포함된 PrivateAccessor&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;프로젝트홈 : &lt;a href=&quot;http://sourceforge.net/projects/junit-addons&quot; class=&quot;bare&quot;&gt;http://sourceforge.net/projects/junit-addons&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API 문서 : &lt;a href=&quot;http://junit-addons.sourceforge.net/junitx/util/PrivateAccessor.html&quot; class=&quot;bare&quot;&gt;http://junit-addons.sourceforge.net/junitx/util/PrivateAccessor.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;샘플코드 : &lt;a href=&quot;http://entireboy.egloos.com/4030723&quot; class=&quot;bare&quot;&gt;http://entireboy.egloos.com/4030723&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;3_직접_reflection_활용&quot;&gt;(3) 직접 Reflection 활용&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package edu.tdd;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.junit.Test;

public class ReflectionCallUtilsTest {

    @Test
    public void testCallPrivate() throws SecurityException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        UnderTest ut = new UnderTest();
        String name = &quot;jsh&quot;;
        String address = &quot;서울시 마포구&quot;;
        invoke(ut, &quot;print&quot;,name, address);
        assertThat(ut.isCalled(),is(true));
    }

    @Test
    public void testCallWithPrimitiveType() throws SecurityException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        UnderTest ut = new UnderTest();
        String name = &quot;jsh&quot;;
        int age = 35;
        boolean printed = (Boolean)invoke(ut, &quot;print&quot;, new Class&amp;lt;?&amp;gt;[]\{String.class, int.class}, name,age);
        assertThat(printed,is(true));
        assertThat(ut.isCalled(),is(true));
    }
    private Object invoke(Object ut, String methodName, Class&amp;lt;?&amp;gt;[] argTypes, Object ... args) throws SecurityException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method method = ut.getClass().getDeclaredMethod(methodName, argTypes);
        method.setAccessible(true);
        return method.invoke(ut, args);
    }

    private Object invoke(Object ut, String methodName, Object ... args) throws SecurityException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        int argSize = args.length;
        Class&amp;lt;?&amp;gt;[] argTypes = new Class&amp;lt;?&amp;gt;[argSize];
        for(int i=0;i&amp;lt; argSize;i++){
            argTypes[i] = args[i].getClass();
        }
        return invoke(ut,methodName, argTypes, args);
    }

    static class UnderTest {
        private boolean called = false;
        public boolean isCalled(){
            return called;
        }
        private void print(String name, String address) {
            System.out.println(name);
            System.out.println(address);
            called = true;
        }
        private boolean print(String name, int age){
            System.out.println(name);
            System.out.println(age);
            called = true;
            return true;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;필요하다면 test 코드 안에서 쓰이고 있는 invoke메서드를 따로 Util 클래스로 분리할 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 코드에서 &lt;code&gt;Object .. args&lt;/code&gt; 넘어가는 부분이 primitive type이 포함되면 &lt;code&gt;Object[]&lt;/code&gt; 로 바뀌는 과정에서 Wrapper class로 바뀌는  auto-boxing이 발생하게 됩니다. 그래서 매개변수에 primitive type이 있을 때는 &lt;code&gt;invoke(Object ut, String methodName, Object &amp;#8230;&amp;#8203; args)&lt;/code&gt; 사용하면 NoSuchMethodException이 발생하게 됩니다. 그럴 때는 type을 정확히 명시해 주는 &lt;code&gt;invoke(Object ut, String methodName, Class&amp;lt;?&amp;gt;[] argTypes, Object &amp;#8230;&amp;#8203; args)&lt;/code&gt; 을 사용하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.artima.com/suiterunner/private.html&quot;&gt;Testing Private Methods with JUnit and SuiteRunner&lt;/a&gt; : 위의 글과 비슷하게 아래 4가지 방법을 제시하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Don&amp;#8217;t test private methods.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Give the methods package access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use a nested test class.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use reflection.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/24/goods/3908398&quot;&gt;채수원 저 테스트 주도개발&lt;/a&gt; 441페이지 : public을 테스트함으로서 간접적으로 테스트하는 방식을 권장하고 굳이 한다면 Reflection을 사용할 수 있다는 점을 언급하고 있으나 부서지기 쉬운 테스트 코드가 되기 쉬움을 경고 하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://xper.org/wiki/xp/TestingPrivateInterfaces&quot; class=&quot;bare&quot;&gt;http://xper.org/wiki/xp/TestingPrivateInterfaces&lt;/a&gt; : Xper에서 논의된 내용&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>로또 번호 생성기 파이썬으로 만들어보기</title>
      <link>https://blog.benelog.net//2669675.html</link>
      <pubDate>Sat, 28 Aug 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2669675.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오래 전에 블로그에 Java를 이용한 로또 번호 생성기(&lt;a href=&quot;http://blog.benelog.net/1646013&quot; class=&quot;bare&quot;&gt;http://blog.benelog.net/1646013&lt;/a&gt; )코드를 올린 적이 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;갑자기 생각이 나서, 이번에는 Python으로 코딩해봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1부터 45까지의 숫자 중에 6개를 뽑는다.6개의 값이 다 달라야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;출력시 작은 숫자부터 순서대로 출력&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;import random
balls = range(1,46)
random.shuffle(balls)
print sorted(balls[0:6])&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>BeanUtils 성능비교 - Apache commons, Opensymphony, Spring</title>
      <link>https://blog.benelog.net//2626007.html</link>
      <pubDate>Thu, 17 Jun 2010 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2626007.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;수정이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2019.04.14&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;테스트한 코드를 &lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/beanutils-test&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/blog/tree/main/examples/beanutils-test&lt;/a&gt; 로 올림&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2010.06.21&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://colus.egloos.com/&quot;&gt;EP&lt;/a&gt;님의 지적으로 Apache commons Beanutils에서도 &lt;a href=&quot;http://commons.apache.org/beanutils/api/org/apache/commons/beanutils/PropertyUtils.html#getPropertyDescriptor(java.lang.Object,%20java.lang.String)&quot;&gt;PropertyUtils.getPropertyDescriptor&lt;/a&gt;를 사용한 방식으로 비교해봤습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.tuning-java.com/&quot;&gt;이상민님&lt;/a&gt;의 지적으로 직접 Bean의 setter를 호출하는 방식의 자료도 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;BeanUtils.populate의 경우 다양한 type을 가진 property 변환을 하는 등의  실제로 많은 기능을 수행함을 설명 추가를 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;div class=&quot;title&quot;&gt;덧붙임&lt;/div&gt;
&lt;p&gt;2019년 2월 시점에 다시 이글을 보니, JMH로 테스트하는 것이 좋았을것 같다는 생각이 듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;조사_내용&quot;&gt;조사 내용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;getter, setter가 있는 Java Bean들의 property들을 복사할 때 apache commons  beanutils(&lt;a href=&quot;http://commons.apache.org/beanutils/&quot; class=&quot;bare&quot;&gt;http://commons.apache.org/beanutils/&lt;/a&gt; ) 가 많이 쓰이고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Spring이나 Opensympony 쪽에도 간단한 BeanUtils가 따로 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spring BeanUtils : &lt;a href=&quot;http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/beans/BeanUtils.html&quot; class=&quot;bare&quot;&gt;http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/beans/BeanUtils.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Opensymphony BeanUtils  : &lt;a href=&quot;http://www.opensymphony.com/oscore/api/com/opensymphony/util/BeanUtils.html&quot; class=&quot;bare&quot;&gt;http://www.opensymphony.com/oscore/api/com/opensymphony/util/BeanUtils.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이러한 BeanUtils들은 reflection을 내부적으로 쓰고 있으니 성능이 안 좋을 것이라는 우려를 할 수 있습니다. 어느 정도 성능 손해는 개발 편의성을 위해서 희생할 수 있는 경우도 많습니다. 그런데, 유사한 용도로 Bean복사를 하더라도 Apache commons BeanUtils쪽이 성능이 더 안 좋다는 소문은 있고 아래 자료에 의하면 100k byte object를 다룰 때는 Spring의 beanUtils에 비하면 거의 5~6배정도의 성능차이가 나는 것으로 알려져있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.christianschenk.org/blog/java-bean-mapper-performance-tests/&quot; class=&quot;bare&quot;&gt;http://www.christianschenk.org/blog/java-bean-mapper-performance-tests/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이는 commons BeanUtils가 단순히 bean을 복사하는 것보다는 많은 기능을 가진 상위수준의 라이브러리 이기 때문에 당연한 결과라고 보여집니다.
Spring이나 Opensymphony의 BeanUtils는 단순히 Bean간의 데이터 복사, 속성정보를 얻어오는 정도인데, apache commons의 BeanUtils는 DynaBean 등 보다 넓은 개념들을 확장해서 제공하고 있습니다.
그리고 BeanUtils.populate는 복사하려는 type이 정확하게 일치하지 않을 경우에도 복사를 해주는 등, 더 확장된 기능을 제공하고 있습니다.
(첨부한 소스의 BeanConverterTypeConversionTest 파일을 보면 Map에 String으로 들어간 속성이 Bean에는 Integer, BigDecimal로 선언되어 있을 때, BeanUtils.populate는 제대로 복사를 해주는 것을 보여줍니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다면 type이 일치하는 단순한 Bean간의 복사등에는 굳이 apache commons beanutils까지 사용할 필요가 없은 경우도 많을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래에서는 &lt;code&gt;List&amp;lt;Map&amp;gt;&lt;/code&gt; &amp;#8594; &lt;code&gt;List&amp;lt;Bean&amp;gt;&lt;/code&gt; 로 변환을 시켜주는 기능을 각각의 library로 구현해서 성능비교를 해보았습니다. 소스들은 Eclipse project형태로 첨부파일에 들어가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;비교군은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;By Hand : 직접 map.get과 bean의 setter를 이용해서 복사&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Spring(property descriptor)  : Spring의 BeanUtils.getPropertyDescriptors를 사용. Spring의 BeanUtils에는 직접적으로 Map에서 Bean으로 복사해주는 메서드가 없어서 이 방식을 이용했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apache Commons(property descriptor) : 2번과 같은 방식을 사용하고, 라이브러리만 Apache commons BeanUtils의 PropertyUtils.getPropertyDescriptor를 사용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apache Commons(populate) : Apache comons BeanUtils의 BeanUtils.populate 메서드를 사용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OpenSymphony(setValues) : Opensymphony의 BeanUtils.setValues를 사용&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실행_시간_비교_결과&quot;&gt;실행 시간 비교 결과&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1만번부터 10만번까지 변환갯수를 늘여주면서(X축), 밀리세컨트 단위로 간단히 수행시간(Y축)을 측정했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/beanutils/graph-1.jpg&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Opensymphony 쪽이 너무 많이 차이가 나서 제외하고 나머지 4개만 다시 그려봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/beanutils/graph-2.jpg&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;col style=&quot;width: 25%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-right valign-top&quot;&gt;size&lt;/th&gt;
&lt;th class=&quot;tableblock halign-right valign-top&quot;&gt;By hand&lt;/th&gt;
&lt;th class=&quot;tableblock halign-right valign-top&quot;&gt;Spring (property descriptor)&lt;/th&gt;
&lt;th class=&quot;tableblock halign-right valign-top&quot;&gt;Apache Commons (property descriptor)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;0&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;10,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;78&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;63&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;20,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;32&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;93&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;125&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;30,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;141&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;141&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;40,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;32&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;188&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;187&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;50,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;218&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;250&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;60,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;15&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;250&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;297&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;70,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;32&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;297&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;359&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;80,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;32&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;359&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;360&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;90,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;46&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;406&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;438&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;100,000&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;63&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;454&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-right valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;484&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;결론&quot;&gt;결론&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;19개의 속성을 가진 Bean을 대상으로 했을 때, Map&amp;#8594;Bean 변환의 10만건의 경우 순위는 아래와 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;By Hand &amp;gt; Spring(property descriptor) = Apache Commons(property descriptor) &amp;gt; Apache Commons(populate) &amp;gt; OpenSymphony(setValues)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;직접 손으로 한 것이 Sprng BeanUtils나 Commons의 PropertyUtils로 propery descriptor를 통해 호출한 것보다 7배 이상 빨랐습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 PropertyDescriptor를 활용한 방식들이 그 다음 순위로, 비슷한 실행속도가 나왔습니다. Spring BeanUtils쪽에서는 Bean객체의 정보를 &lt;a href=&quot;http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/beans/CachedIntrospectionResults.html&quot;&gt;CachedIntrospectionResults&lt;/a&gt;라는 클래스에 저장을 해 두고 있습니다. 그리고 Apache commons의 PropertyUtils에서도 유사하게 PropertyUtilsBean안에 descriptorsCache라는 속성으로 Bean정보를 Cache하고 있습니다. 그래서 실행속도가 거의 비슷하게 나온듯 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Apache commons BeanUtils.populate는 2,3위 순위의 Property descriptor를 활용한 것들보다  6배 정도 실행시간이 더 걸리는 것으로 나왔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BeanUtils.populate가 더 느린 이유는 두가지로 분석이 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;위에서 말한 것처럼 좀 더 확장된 type변환을 지원하기 떄문입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;PropertyDescriptor의 배열을 순회하는 방식이 아닌, Map의 keySet을 순회하는 방식을 쓰고 있는데, 아무래도 배열의 순회보다는 성능에는 불리할 것 같습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Iterator names = properties.keySet().iterator();
while (names.hasNext()) {
    // Identify the property name and value(s) to be assigned
    String name = (String) names.next();
    if (name == null) {
        continue;
    }
    Object value = properties.get(name);
    // Perform the assignment for this property
    setProperty(bean, name, value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Opensymphony쪽은 70배이상 더 느린데, 제가 구현한 방식이 문제가 있는 건지도 모르겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아뭏든 위의 결과를 봐서는 되도록 성능이 민감한 곳에는 직접 setter를 호출해서 복사를 하고, BeanUtils.populate의 다양한 기능이 필요하지 않다면 Spring의 BeanUtils나 Apache commons PropertyUtils를 통해 캐시된 PropertyDescriptor를 정보를 통해 Bean에 접근하는 것이 성능에는 유리하다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;소스&quot;&gt;소스&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/benelog/blog/tree/main/examples/beanutils-test&quot; class=&quot;bare&quot;&gt;https://github.com/benelog/blog/tree/main/examples/beanutils-test&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Apache commons BeanUtils의 BeanUtils.populate 활용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public &amp;lt;T extends Map&amp;lt;String,Object&amp;gt;, C&amp;gt; List&amp;lt;C&amp;gt; convertMapToBean(List&amp;lt;T&amp;gt; list,
  Class&amp;lt;C&amp;gt; clazz) {
    List&amp;lt;C&amp;gt; beanList = new ArrayList&amp;lt;C&amp;gt;();

    for (T item : list) {
        C bean = null;
        try {
            bean = clazz.newInstance();
            BeanUtils.populate(bean, item);
        } catch (InstantiationException e) {
            new IllegalArgumentException(&quot;Cannot initiate class&quot;,e);
        } catch (IllegalAccessException e) {
            new IllegalStateException(&quot;Cannot access the property&quot;,e);
        } catch (InvocationTargetException e) {
            new IllegalArgumentException(e);
        }
        beanList.add(bean);
    }
    return beanList;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Apache commons BeanUtils : PropertyUtils.getPropertyDescriptors활용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public &amp;lt;T extends Map&amp;lt;String, Object&amp;gt;, C&amp;gt; List&amp;lt;C&amp;gt; convertMapToBean(
    List&amp;lt;T&amp;gt; list, Class&amp;lt;C&amp;gt; clazz) {
    List&amp;lt;C&amp;gt; beanList = new ArrayList&amp;lt;C&amp;gt;();

    for (T source : list) {
        C bean = null;

        try {
            bean = clazz.newInstance();

            PropertyDescriptor[] targetPds = PropertyUtils.getPropertyDescriptors(clazz);

            for (PropertyDescriptor desc : targetPds) {
                Object value = source.get(desc.getName());
                if (value != null) {
                    Method writeMethod = desc.getWriteMethod();
                    if (writeMethod != null) {
                        writeMethod.invoke(bean, new Object[] { value });
                    }
                }
            }
        } catch (InstantiationException e) {
            new IllegalArgumentException(&quot;Cannot initiate class&quot;,e);
        } catch (IllegalAccessException e) {
            new IllegalStateException(&quot;Cannot access the property&quot;,e);
        } catch (InvocationTargetException e) {
            new IllegalArgumentException(e);
        }
        beanList.add(bean);
    }
    return beanList;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;OpenSymphony BeanUtils : setValues 활용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public &amp;lt;T extends Map&amp;lt;String, Object&amp;gt;, C&amp;gt; List&amp;lt;C&amp;gt; convertMapToBean(List&amp;lt;T&amp;gt; list, Class&amp;lt;C&amp;gt; targetClass) {
    List&amp;lt;C&amp;gt; beanList = new ArrayList&amp;lt;C&amp;gt;();

    for (Map&amp;lt;String, Object&amp;gt; map : list) {
        C bean = null;
        try {
            bean = targetClass.newInstance();
            BeanUtils.setValues(bean, map, null);
        } catch (InstantiationException e) {
            new IllegalArgumentException(&quot;Cannot initiate class&quot;, e);
        } catch (IllegalAccessException e) {
            new IllegalStateException(&quot;Cannot access the property&quot;, e);
        }
        beanList.add(bean);
    }
    return beanList;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Spring BeanUtils : getPropertyDescriptors 활용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public &amp;lt;T extends Map&amp;lt;String, Object&amp;gt;, C&amp;gt; List&amp;lt;C&amp;gt; convertMapToBean(
    List&amp;lt;T&amp;gt; list, Class&amp;lt;C&amp;gt; clazz) {
    List&amp;lt;C&amp;gt; beanList = new ArrayList&amp;lt;C&amp;gt;();
    for (Map&amp;lt;String, Object&amp;gt; source : list) {
        C bean = toBean(source, clazz);
    beanList.add(bean);

    }
    return beanList;
}

private &amp;lt;C&amp;gt; C toBean(Map&amp;lt;String, Object&amp;gt; source, Class&amp;lt;C&amp;gt; targetClass) {
    C bean = null;
    try {
        bean = targetClass.newInstance();
        PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(targetClass);

        for (PropertyDescriptor desc : targetPds) {
            Object value = source.get(desc.getName());
            if (value != null) {
                Method writeMethod = desc.getWriteMethod();
                if (writeMethod != null) {
                    writeMethod.invoke(bean, new Object[] { value });
                }
            }
        }
    } catch (InstantiationException e) {
        new IllegalArgumentException(&quot;Cannot initiate class&quot;,e);
    } catch (IllegalAccessException e) {
        new IllegalStateException(&quot;Cannot access the property&quot;,e);
    } catch (InvocationTargetException e) {
        new IllegalArgumentException(e);
    }
    return bean;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;성능측정_코드&quot;&gt;성능측정 코드&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@Test
public void testApacheCommonsBeanUtils() {
    BeanConverter converter = new ApacheCommonsBeanUtilsBeanConverter();
    executeIncrementally(converter);
}

@Test
public void testApacheCommonsPropertyUtils() {
    BeanConverter converter = new ApacheCommonsPropertyUtilsBeanConverter();
    executeIncrementally(converter);
}

@Test
public void testOpenSymphony() {
    BeanConverter converter = new OpenSymphonyBeanConverter();
    executeIncrementally(converter);
}

@Test
public void testSpring() {
    BeanConverter converter = new SpringBeanConverter();
    executeIncrementally(converter);
}

@Test
public void testByHand() {
    BeanConverter converter = new UserConverter();
    executeIncrementally(converter);
}

private void excuecteBeanConverter(BeanConverter converter, int iterations) {
    List&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; testList = createMapListForTest(iterations);
    long start = System.currentTimeMillis();
    List&amp;lt;User&amp;gt; beanList = converter.convertMapToBean(testList, User.class);
    long end = System.currentTimeMillis();
    System.out.printf(&quot;%s,%d times, %d milliseconds \r\n&quot;, converter.getClass().getSimpleName(), iterations, (end - start));
}

private void executeIncrementally(BeanConverter converter) {
    for (int i = 0; i &amp;lt;= 100000; i += 10000) {
        excuecteBeanConverter(converter, i);
    }
}

private List&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; createMapListForTest(int iterations) {
    List&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; list = new ArrayList&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt;();

    Map&amp;lt;String, Object&amp;gt; user = new HashMap&amp;lt;String, Object&amp;gt;();
    user.put(&quot;id&quot;, 1);
    user.put(&quot;age&quot;, 1);
    user.put(&quot;name&quot;, &quot;내이름&quot;);
    user.put(&quot;name1&quot;, &quot;내이름&quot;);
    user.put(&quot;name2&quot;, &quot;내이름&quot;);
    user.put(&quot;name3&quot;, &quot;내이름&quot;);
    user.put(&quot;name4&quot;, &quot;내이름&quot;);
    user.put(&quot;name5&quot;, &quot;내이름&quot;);
    user.put(&quot;name6&quot;, &quot;내이름&quot;);
    user.put(&quot;name7&quot;, &quot;내이름&quot;);
    user.put(&quot;name8&quot;, &quot;내이름&quot;);
    user.put(&quot;name9&quot;, &quot;내이름&quot;);
    user.put(&quot;name10&quot;, &quot;내이름&quot;);
    user.put(&quot;income&quot;, new BigDecimal(&quot;1000100100&quot;));
    user.put(&quot;address&quot;, &quot;오늘 아침 내가 행복한 이유는 이런거지 오늘아침 내가 서러운 이유는 그런거야 &quot;);
    user.put(&quot;introduce&quot;, &quot;오늘 아침 내가 행복한 이유는 이런거지 오늘아침 내가 서러운 이유는 그런거야 &quot;);
    user.put(&quot;married&quot;, true);
    user.put(&quot;nickName&quot;, &quot;뻐꾸기&quot;);

    for (int i = 0; i &amp;lt; iterations; i++) {
        list.add(user);
    }
    return list;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Customer tag library에 대한 테스트 코드 작성</title>
      <link>https://blog.benelog.net//2421450.html</link>
      <pubDate>Tue, 8 Sep 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2421450.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JSP의 커스텀 태그 라이브러리는 그 실행결과를 확인하는 것이 많이 번거롭습니다. 따로 테스트 코드를 짜지 않는다면, Web Application 를 띄우고 커스템 태그를 사용하는 JSP를 직접 실행한 다음에 나오는 텍스트값을 확인해서 눈으로 값이 제대로 찍히는지 검증하고, 틀리면 다시 코드를 고치는 방식을 반복하는 경우도 많습니다. 그리고 보통 커스텀태그에서는 setter로 지정된 속성에 따라서 조건분기도 많이 들어가기 때문에 더욱 디버깅이 까다롭습니다. 이 때 위와 같이 JSP를 거치지 않고 바로 출력될 값을 찎어주고, 검증로직을 추가할 수 있는 테스트 코드를 짠다면 개발할 때 많은 시간이 절약될 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.docjar.org/docs/api/javax/servlet/jsp/tagext/TagSupport.html&quot;&gt;javax.servlet.jsp.tagext.TagSupport&lt;/a&gt;를 상속한 클래스라면, TagSupport.setPageContext 메서드를 활용해서 mock같은 테스트용 객체들을 삽입하면 됩니다. 이 메서드를 통해서 PageContext와 PageContext.getOut으로 돌려주는 javax.servlet.jsp.JspWriter객체를 모두 mock으로 지정할 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring에서는 이를 더욱 간편하게 할 수 있는 &lt;a href=&quot;http://static.springsource.org/spring/docs/2.5.6/api/org/springframework/mock/web/MockPageContext.html&quot;&gt;MockPageContext&lt;/a&gt;라는 객체를 제공합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이를 활용한 Custom 태그 테스트 코드는 아래와 같이 만들 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;ButtonTag tag = new ButtonTag();
tag.setFunctionName(&quot;alert&quot;);
tag.setType(&quot;basic&quot;);

PageContext pageContext = new MockPageContext();
tag.setPageContext(pageContext);

assertEquals(TagSupport.EVAL_BODY_INCLUDE, tag.doStartTag());
assertEquals(TagSupport.EVAL_PAGE, tag.doEndTag());
String output = ((MockHttpServletResponse) pageContext.getResponse()).getContentAsString();
System.out.println(output);
assertTrue(output.contains(&quot;&amp;lt;span class=&apos;r&apos;&amp;gt;&quot;));
//출력된 결과에 대한 추가  검증&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Oracle을 사용해 입출력하는 Map-Reduce</title>
      <link>https://blog.benelog.net//2271302.html</link>
      <pubDate>Wed, 25 Mar 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2271302.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hadoop의 Map-Reduce처리에서는 DB를 바로 연결해서 처리할 수 있는  &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBInputFormat.html&quot;&gt;DBInputFormat&lt;/a&gt;, &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBOutputFormat.html&quot;&gt;DBOutputFormat&lt;/a&gt;의 클래스가 제공되고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 이 클래스들은 이름이 &apos;DB&amp;#8217;가 붙어있는 것이 무색하게 Oracle과 연결해서 사용해보면 에러가 납니다. &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBInputFormat.html&quot;&gt;DBInputFormat&lt;/a&gt;에서는 웹에서의 페이지 처리 쿼리처럼 데이터를 잘라서 가지고 오기 위해 원래 쿼리에다 LIMIT와 OFFSET 키워드를 붙이는데, 이 것은 Oracle에서는 지원되지 않습니다. 그리고 &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBOutputFormat.html&quot;&gt;DBOutputFormat&lt;/a&gt;에서는 insert문의 맨 뒤에 세미콜론(;)을 붙여버리는데, 이것 역시 Oracle의 JDBC를 사용할 때는 에러를 냅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서, 결국 이 클래스들을 Oracle에서 쓸 수 있도록 상속해서 구현을 해 줄수 밖에 없었습니다. 얼핏 생각하면 쿼리만 바꾸어주면 되는 것이니 메서드 하나만 오버라이딩 해주면 될 것으로 예상했으나, 원래 클래스들의 구조가 그 정도로 단순하지 않았습니다.Inner클래스가 많아서 여러 클래스와 메서드들을 다 overriding해 줄 수 밖에 없었습니다. 더군다나, 새로 상속한 클래스의 내부에서 꼭 호출해야 하는 DBConfiguration클래스의 생성자가 public이 아닌 package private(아무것도 선언안한 디폴트 접근자)인 탓에, 패키지를 원래의 &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBInputFormat.html&quot;&gt;DBInputFormat&lt;/a&gt;, &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBOutputFormat.html&quot;&gt;DBOutputFormat&lt;/a&gt;와 같은 패키지로 맞추어야 하는 불편함도 있었습니다. protected로 선언된 메서드들이 많은 것보면 분명히 상속해서 덮어쓰라고 만들어놓은 클래스 같은데, 막상 그렇게 활용하기에는 간편하지 않았던 것이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 또 구조적으로 아쉬운 점은 두 클래스가 같은 DBConfiguration을 보게 있어서 Map에서 입력자료를 얻어오는 DB와 Reduce에서 쓰는 DB가 다를 때는 다시 별도의 클래스를 만들어주어야 한다는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Batch에서도 &lt;a href=&quot;http://static.springsource.org/spring-batch/apidocs/org/springframework/batch/item/database/JdbcPagingItemReader.html&quot;&gt;JdbcPagingItemReader&lt;/a&gt;라는 약간 유사한 클래스가 있습니다. &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBInputFormat.html&quot;&gt;DBInputFormat&lt;/a&gt;이 하나의 쿼리에서 가지고 올 데이터를 동시에 여러번 쿼리해서 나누어 가지고 오는 반면에 &lt;a href=&quot;http://static.springsource.org/spring-batch/apidocs/org/springframework/batch/item/database/JdbcPagingItemReader.html&quot;&gt;JdbcPagingItemReader&lt;/a&gt;에서는 부분씩 가지고 오더라도 순차적으로 쿼리를 하는 차이점이 있기는 합니다. 그래도, 페이지 처리 쿼리처럼, 데이터를 나누어서 가지고 오는 쿼리를 제공한다는 점에서는 유사합니다. &lt;a href=&quot;http://static.springsource.org/spring-batch/apidocs/org/springframework/batch/item/database/JdbcPagingItemReader.html&quot;&gt;JdbcPagingItemReader&lt;/a&gt;에서는 내부적으로 &lt;a href=&quot;http://static.springsource.org/spring-batch/apidocs/org/springframework/batch/item/database/PagingQueryProvider.html&quot;&gt;PagingQueryProvider&lt;/a&gt; 라는 인터페이스를 사용하게 되어 있고, 이 인터페이스는 각 DB종류별로 OraclePagingQueryProvider, HsqlPagingQueryProvider, MySqlPagingQueryProvider, SqlServerPagingQueryProvider, SybasePagingQueryProvider 등의 구현클래스를 가지고 있습니다.
Hadoop의 &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/lib/db/DBInputFormat.html&quot;&gt;DBInputFormat&lt;/a&gt;도 이런 구조였다면 이를 응용하려는 개발자가 훨씬 쉽게 클래스 확장방법을  이해했을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아뭏든 지금까지 현재 공개된 API만으로는 Hadoop의 DB연결 지원 클래스들은 빈약해 보이고, API도 좋은 설계요건을 갖추었다고 느껴지지는 않습니다. 아무래도 포털 등에서 대용량 데이터를 처리하는 곳에 쓰이다보니 DB와 함께 연결되는 쓰임새가 그리 많지는 않았나봅니다. 더군다나 Oracle에서는 한번도 안 돌려본 클래스가 버젓이 DB&amp;#8230;&amp;#8203;로 시작되는 이름으로 들어간 것 보면 Oracle이 쓰이는 동네와 Hadoop이 사는 곳은 아주 멀리 떨어져 있었던 것 같습니다. 그러나, 앞으로  엔터프라이즈 환경에서도 Hadoop이 쓰이려면 DB와의 integration은 반드시 거쳐야할 다리인 것 같습니다. &lt;a href=&quot;http://www.jaso.co.kr/283&quot;&gt;Enterprise 시장에서의 mapreduce&lt;/a&gt; 링크를 보아도 이미 그런 시도들이 시작된 것을 알 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한편, Hadoop의 &lt;a href=&quot;http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/mapred/FileInputFormat.html&quot;&gt;FileInputFormat&lt;/a&gt;가 Spring batch의 &lt;a href=&quot;http://static.springsource.org/spring-batch/apidocs/org/springframework/batch/item/file/FlatFileItemReader.html&quot;&gt;FlatFileItemReader&lt;/a&gt;와 유사한 것 등이나 Spring batch도 2.0에서 아직 분산, 동시처리 등을 지원하기 시작했다는 점은 두 프레임웍의 겹치는 지점이 늘어날 수도 있다는 생각도 듭니다. 뭐 아직 Spring batch의 분산지원은 걸음마 단계이기는 합니다만, DB에서 HDFS에 들어가는 파일을 쓸 때 Spring batch의 API를 활용하는 것 같은 활용법은 시도해 볼만하다고 생각됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;소스&quot;&gt;소스&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;OracleInputFormat&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package org.apache.hadoop.mapred.lib.db;

import java.io.IOException;
import java.sql.SQLException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapred.InputSplit;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.RecordReader;
import org.apache.hadoop.mapred.Reporter;

public class OracleInputFormat&amp;lt;T extends DBWritable&amp;gt; extends DBInputFormat&amp;lt;T&amp;gt;{

    private DBConfiguration dbConf = null;
    private DBInputSplit split;

    @Override
    public RecordReader&amp;lt;LongWritable,T&amp;gt; getRecordReader(InputSplit split, JobConf job,
            Reporter reporter) throws IOException {
        dbConf = new DBConfiguration(job);
        this.split = (DBInputSplit)split;

        @SuppressWarnings(&quot;unchecked&quot;)
        Class inputClass = dbConf.getInputClass();
        try {
            @SuppressWarnings(&quot;unchecked&quot;)
            RecordReader&amp;lt;LongWritable,T&amp;gt; reader = new OracleRecordReader((DBInputSplit) split, inputClass, job);
            return reader;
        } catch (SQLException ex) {
            throw new IOException(ex.getMessage());
        }
    }

    public static void setInput(JobConf job, Class&amp;lt;? extends DBWritable&amp;gt; inputClass,
              String inputQuery, String inputCountQuery) {
        DBInputFormat.setInput(job, inputClass, inputQuery, inputCountQuery);
        job.setInputFormat(OracleInputFormat.class);
     }

    protected class OracleRecordReader extends DBRecordReader{
        protected OracleRecordReader(DBInputSplit split, Class&amp;lt;T&amp;gt; inputClass,
                JobConf conf) throws SQLException {
            super(split, inputClass, conf);
        }

        @Override
        protected String getSelectQuery() {
            long length = 0;
            long start = 0;
            try{
                length = split.getLength();
                start = split.getStart();
            } catch(IOException e){
                throw new IllegalArgumentException
                        (&quot;cannot read length or start variable from DBInputSplit&quot;,e);
            }
            StringBuilder query = new StringBuilder();
            query.append(&quot; SELECT * \r\n&quot;);
            query.append(&quot; FROM (SELECT m.* , ROWNUM rno &quot;);
            query.append(&quot;       FROM ( &quot;);
            query.append(              dbConf.getInputQuery());
            query.append(&quot;             )  m&quot;);
            query.append(&quot;       WHERE ROWNUM &amp;lt;= &quot; + start + &quot; + &quot; + length + &quot;)&quot;);
            query.append(&quot; WHERE RNO &amp;gt; &quot; + start);
            System.out.println(query.toString());
            return query.toString();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;OracleOutputFormat&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package org.apache.hadoop.mapred.lib.db;
import org.apache.hadoop.mapred.JobConf;

public class OracleOutputFormat&amp;lt;K  extends DBWritable, V&amp;gt; extends DBOutputFormat&amp;lt;DBWritable, V&amp;gt;{
    @Override
    protected String constructQuery(String table, String[] fieldNames) {
            if(fieldNames == null) {
              throw new IllegalArgumentException(&quot;Field names may not be null&quot;);
            }
            StringBuilder query = new StringBuilder();
            query.append(&quot;INSERT INTO &quot;).append(table);

            if (fieldNames.length &amp;gt; 0 &amp;amp;&amp;amp; fieldNames[0] != null) {
              query.append(&quot; (&quot;);
              for (int i = 0; i &amp;lt; fieldNames.length; i++) {
                query.append(fieldNames[i]);
                if (i != fieldNames.length - 1) {
                  query.append(&quot;,&quot;);
                }
              }
              query.append(&quot;)&quot;);
            }
            query.append(&quot; VALUES (&quot;);

            for (int i = 0; i &amp;lt; fieldNames.length; i++) {
              query.append(&quot;?&quot;);
              if(i != fieldNames.length - 1) {
                query.append(&quot;,&quot;);
              }
            }
            query.append(&quot;)&quot;);
            return query.toString();
          }
    public static void setOutput(JobConf job, String tableName, String... fieldNames) {
        DBOutputFormat.setOutput(job, tableName, fieldNames);
        job.setOutputFormat(OracleOutputFormat.class);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Job 구성 예&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class SampleJob {

    public static void main(String args[]) throws IOException, URISyntaxException{
        JobConf conf = new JobConf(SampleJob.class);
        initClasspath(conf);
        conf.setJobName(&quot;sampleJob&quot;);
        DBConfiguration.configureDB(conf, &quot;oracle.jdbc.driver.OracleDriver&quot;,
                &quot;jdbc:oracle:thin:@localhost:1525:TEST&quot;,
                &quot;myuser&quot;, &quot;mypassword&quot;);
        OracleInputFormat.setInput(conf, Query.class,
                &quot;SELECT query, category, user_id FROM query_log &quot;,
                &quot;SELECT COUNT(*) FROM query_log&quot;);
        conf.setOutputKeyClass(Query.class);
        conf.setOutputValueClass(IntWritable.class);
        conf.setMapperClass(SampleMapper.class);
        conf.setReducerClass(SampleReducer.class);
        conf.setCombinerClass(SampleReducer.class);
        OracleOutputFormat.setOutput(conf, &quot;category&quot;, &quot;user_id&quot;,&quot;cnt&quot;);

        JobClient.runJob(conf);
    }

    private static void initClasspath(JobConf conf) throws URISyntaxException,
            IOException {
        DistributedCache.addCacheFile(new URI(&quot;lib/ojdbc5-11.1.0.6.jar&quot;), conf);
        DistributedCache.addFileToClassPath(new Path(&quot;lib/ojdbc5-11.1.0.6.jar&quot;), conf);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hadoop의_dbinputformat_참고자료&quot;&gt;hadoop의 DBInputFormat 참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://developer.yahoo.net/blogs/hadoop/DBInputFormat.ppt&quot; class=&quot;bare&quot;&gt;http://developer.yahoo.net/blogs/hadoop/DBInputFormat.ppt&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.cloudera.com/blog/wp-content/uploads/DBInputFormat.pdf&quot; class=&quot;bare&quot;&gt;http://www.cloudera.com/blog/wp-content/uploads/DBInputFormat.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.cloudera.com/blog/2009/03/06/database-access-with-hadoop/&quot; class=&quot;bare&quot;&gt;http://www.cloudera.com/blog/2009/03/06/database-access-with-hadoop/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.cloudera.com/blog/tag/dbinputformat/&quot; class=&quot;bare&quot;&gt;http://www.cloudera.com/blog/tag/dbinputformat/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java auto boxing의 컴파일 방식과 Integer 캐시</title>
      <link>https://blog.benelog.net//java-auto-boxing.html</link>
      <pubDate>Tue, 10 Feb 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">java-auto-boxing.html</guid>
      	<description>
	&lt;div id=&quot;toc&quot; class=&quot;toc&quot;&gt;
&lt;div id=&quot;toctitle&quot;&gt;Table of Contents&lt;/div&gt;
&lt;ul class=&quot;sectlevel1&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#박싱과_언박싱으로_컴파일되는_바이트코드&quot;&gt;박싱과 언박싱으로 컴파일되는 바이트코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#integer_valueof의_캐시와_참조_비교&quot;&gt;Integer.valueOf()의 캐시와 참조 비교&lt;/a&gt;
&lt;ul class=&quot;sectlevel2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#xxautoboxcachemax로_캐시_범위_조정&quot;&gt;-XX:AutoBoxCacheMax로 캐시 범위 조정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_9부터_deprecated된_wrapper_생성자&quot;&gt;JDK 9부터 deprecated된 wrapper 생성자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#박싱된_타입을_반복문에_쓸_때의_비용&quot;&gt;박싱된 타입을 반복문에 쓸 때의 비용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#캐시가_없는_double와_float&quot;&gt;캐시가 없는 Double와 Float&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jdk_버전별_변경_이력&quot;&gt;JDK 버전별 변경 이력&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#정리&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#참고_자료&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt; 와 &lt;code&gt;java.lang.Integer&lt;/code&gt;를 섞어 쓰면 JDK의 &lt;code&gt;javac&lt;/code&gt;이 auto boxing과 unboxing 코드를 대신 넣어 줍니다. 그 변환이 어떤 메서드 호출로 바뀌는지 알아 두면 &lt;code&gt;==&lt;/code&gt; 비교의 결과나 반복문에서 생기는 객체 생성 비용을 예측할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;javac&lt;/code&gt;의 변환 방식 자체는 JDK 5에서 auto boxing이 도입된 뒤로 바뀌지 않았지만, 캐시의 구현과 wrapper 생성자의 deprecation 상태는 버전마다 달라졌습니다. 이 글의 바이트코드와 실행 결과는 JDK 25(Temurin 25+36)에서 확인했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;박싱과_언박싱으로_컴파일되는_바이트코드&quot;&gt;박싱과 언박싱으로 컴파일되는 바이트코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 코드는 &lt;code&gt;int&lt;/code&gt; 와 &lt;code&gt;Integer&lt;/code&gt;를 서로 대입하고 &lt;code&gt;==&lt;/code&gt; 로 비교합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Boxing.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class Boxing {
    public static void main(String[] args) {
        int a = 1;
        Integer b = 1;
        int c = b;
        System.out.println(a == b);
        System.out.println(a == c);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK에 포함된 &lt;code&gt;javap -c Boxing.class&lt;/code&gt; 를 실행하면 다음 바이트코드가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  public static void main(java.lang.String[]);
    Code:
         0: iconst_1
         1: istore_1
         2: iconst_1
         3: invokestatic  #7    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         6: astore_2
         7: aload_2
         8: invokevirtual #13   // Method java/lang/Integer.intValue:()I
        11: istore_3
        12: getstatic     #17   // Field java/lang/System.out:Ljava/io/PrintStream;
        15: iload_1
        16: aload_2
        17: invokevirtual #13   // Method java/lang/Integer.intValue:()I
        20: if_icmpne     27
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;박싱과 언박싱만을 위한 바이트코드 명령은 따로 없습니다. JDK의 &lt;code&gt;javac&lt;/code&gt;은 &lt;code&gt;int&lt;/code&gt; 가 &lt;code&gt;Integer&lt;/code&gt;로 바뀔 때 &lt;code&gt;Integer.valueOf()&lt;/code&gt;를, &lt;code&gt;Integer&lt;/code&gt;가 &lt;code&gt;int&lt;/code&gt; 로 바뀔 때 &lt;code&gt;intValue()&lt;/code&gt;를 호출하는 바이트코드로 컴파일합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;a == b&lt;/code&gt; 처럼 한쪽이 &lt;code&gt;int&lt;/code&gt; 이고 다른 쪽이 &lt;code&gt;Integer&lt;/code&gt;이면 &lt;code&gt;Integer&lt;/code&gt; 쪽이 언박싱되어 값 비교(&lt;code&gt;if_icmpne&lt;/code&gt;)가 됩니다. 반면 양쪽이 모두 &lt;code&gt;Integer&lt;/code&gt;이면 언박싱 없이 참조 비교(&lt;code&gt;if_acmpne&lt;/code&gt;)가 되므로, 다음 절에서 다루는 캐시 범위에 따라 결과가 달라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;역컴파일하면 변환 결과를 Java 코드로 읽을 수 있습니다. 다만 요즘 역컴파일러는 &lt;code&gt;valueOf()&lt;/code&gt;와 &lt;code&gt;intValue()&lt;/code&gt; 호출을 auto boxing 문법으로 복원해서 원본과 거의 같은 코드를 보여 줍니다. &lt;a href=&quot;https://www.benf.org/other/cfr/&quot;&gt;CFR&lt;/a&gt;에서는 &lt;code&gt;--sugarboxing false&lt;/code&gt; 옵션으로 이 복원을 끄고 메서드 호출을 그대로 보여 주게 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java -jar cfr-0.152.jar --sugarboxing false Boxing.class&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class Boxing {
    public static void main(String[] stringArray) {
        int n = 1;
        Integer n2 = Integer.valueOf((int)1);
        int n3 = n2.intValue();
        System.out.println((n == n2.intValue() ? 1 : 0) != 0);
        System.out.println((n == n3 ? 1 : 0) != 0);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Integer.valueOf((int)1)&lt;/code&gt; 의 &lt;code&gt;(int)&lt;/code&gt; 처럼 원본에 없던 캐스팅과 &lt;code&gt;(&amp;#8230;&amp;#8203; ? 1 : 0) != 0&lt;/code&gt; 같은 표현은 역컴파일러가 붙인 것입니다. 바이트코드에는 boolean 값만 다루는 명령이 없어서, &lt;code&gt;javac&lt;/code&gt;은 비교 결과를 &lt;code&gt;if_icmpne&lt;/code&gt; 분기와 0·1 정수로 만듭니다. 역컴파일러는 그 흐름을 그대로 옮겨 이런 모양으로 보여 줍니다. 눈여겨볼 곳은 대입문 두 줄입니다. &lt;code&gt;Integer n2&lt;/code&gt; 에는 &lt;code&gt;Integer.valueOf()&lt;/code&gt;의 반환값이, &lt;code&gt;int n3&lt;/code&gt; 에는 &lt;code&gt;intValue()&lt;/code&gt;의 반환값이 들어갑니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;--sugarboxing&lt;/code&gt; 옵션을 지정하지 않으면 아래처럼 원본 소스와 같은 코드가 나옵니다. 그래서 역컴파일 결과만으로는 박싱 비용을 판단할 수 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java -jar cfr-0.152.jar Boxing.class&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class Boxing {
    public static void main(String[] stringArray) {
        int n = 1;
        Integer n2 = 1;
        int n3 = n2;
        System.out.println(n == n2);
        System.out.println(n == n3);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;integer_valueof의_캐시와_참조_비교&quot;&gt;Integer.valueOf()의 캐시와 참조 비교&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;new Integer(int)&lt;/code&gt; 생성자는 매번 새 객체를 만들지만, &lt;code&gt;Integer.valueOf()&lt;/code&gt;는 캐시된 인스턴스를 재사용할 수 있습니다. JDK 25의 &lt;code&gt;java.lang.Integer&lt;/code&gt; 는 -128부터 127까지의 인스턴스를 &lt;code&gt;IntegerCache&lt;/code&gt; 에 담아 둡니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java/lang/Integer.java (JDK 25)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    private static final class IntegerCache {
        static final int low = -128;
        static final int high;

        @Stable
        static final Integer[] cache;
        static Integer[] archivedCache;

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty(&quot;java.lang.Integer.IntegerCache.high&quot;);
            ...
        }
    }

    public static Integer valueOf(int i) {
        if (i &amp;gt;= IntegerCache.low &amp;amp;&amp;amp; i &amp;lt;= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시의 기본 상한은 127이고, VM이 별도로 보관하는 saved property로 더 늘릴 수 있습니다. CDS(Class Data Sharing) 아카이브를 사용할 수 있으면 아카이브된 배열을 가져옵니다. 아카이브가 없거나 요청된 캐시 크기보다 작으면 캐시 전체 또는 부족한 부분을 실행 시점에 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JLS는 &lt;code&gt;IntegerCache&lt;/code&gt; 라는 구현 자체를 요구하지 않습니다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html#jls-5.1.7&quot;&gt;JLS 5.1.7&lt;/a&gt;은 박싱 대상이 상수식일 때 아래 조건에서 두 박싱 결과가 반드시 같은 참조여야 한다고 규정합니다. JDK는 이 요구를 캐시로 충족합니다. 한편 &lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html#valueOf(int)&quot;&gt;&lt;code&gt;Integer.valueOf(int)&lt;/code&gt;의 API 계약&lt;/a&gt;은 상수식 여부와 관계없이 -128부터 127까지를 항상 캐시한다고 보장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If the value &lt;code&gt;p&lt;/code&gt; being boxed is the result of evaluating a constant expression of type &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;byte&lt;/code&gt;, &lt;code&gt;char&lt;/code&gt;, &lt;code&gt;short&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;, or &lt;code&gt;long&lt;/code&gt;, and the result is &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, a character in the range &lt;code&gt;&apos;\u0000&apos;&lt;/code&gt; to &lt;code&gt;&apos;\u007f&apos;&lt;/code&gt; inclusive, or an integer in the range &lt;code&gt;-128&lt;/code&gt; to &lt;code&gt;127&lt;/code&gt; inclusive, then let &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; be the results of any two boxing conversions of &lt;code&gt;p&lt;/code&gt;. It is always the case that &lt;code&gt;a == b&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; JLS 5.1.7. Boxing Conversion
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시 범위 때문에 아래 코드의 결과는 값의 크기에 따라 달라집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Identity.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class Identity {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        Integer c = 128;
        Integer d = 128;
        System.out.println(&quot;a == b : &quot; + (a == b));
        System.out.println(&quot;c == d : &quot; + (c == d));
        System.out.println(&quot;c.equals(d) : &quot; + c.equals(d));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;a == b : true
c == d : false
c.equals(d) : true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;기본 설정에서 128은 캐시 범위 밖이므로 박싱할 때마다 새 &lt;code&gt;Integer&lt;/code&gt;가 만들어집니다. 그래서 &lt;code&gt;c == d&lt;/code&gt; 는 &lt;code&gt;false&lt;/code&gt; 입니다. 박싱된 타입끼리는 &lt;code&gt;==&lt;/code&gt; 대신 &lt;code&gt;equals()&lt;/code&gt;로 비교해야 하는 이유입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;캐시 범위는 타입마다 다릅니다. JDK 25 기준으로 &lt;code&gt;Long&lt;/code&gt;, &lt;code&gt;Short&lt;/code&gt;, &lt;code&gt;Byte&lt;/code&gt;는 -128~127을, &lt;code&gt;Character&lt;/code&gt;는 &lt;code&gt;&apos;\u0000&apos;&lt;/code&gt; 부터 &lt;code&gt;&apos;\u007f&apos;&lt;/code&gt; 까지를 캐시합니다. &lt;code&gt;Boolean&lt;/code&gt;은 &lt;code&gt;Boolean.TRUE&lt;/code&gt;와 &lt;code&gt;Boolean.FALSE&lt;/code&gt;를 재사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;xxautoboxcachemax로_캐시_범위_조정&quot;&gt;-XX:AutoBoxCacheMax로 캐시 범위 조정&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;Integer&lt;/code&gt;는 HotSpot 옵션인 &lt;code&gt;-XX:AutoBoxCacheMax=&amp;lt;max&amp;gt;&lt;/code&gt; 로 캐시의 상한을 늘릴 수 있습니다. 이 옵션은 C2 컴파일러가 포함된 HotSpot에서 쓸 수 있습니다. 앞의 예제를 같은 클래스 파일로 다시 실행하면 결과가 바뀝니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;shell&quot;&gt;$ java Identity
a == b : true
c == d : false
c.equals(d) : true

$ java -XX:AutoBoxCacheMax=1000 Identity
a == b : true
c == d : true
c.equals(d) : true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 코드라도 실행 옵션에 따라 &lt;code&gt;==&lt;/code&gt; 의 결과가 달라지므로, 박싱된 타입은 &lt;code&gt;==&lt;/code&gt; 로 비교하면 안 됩니다. 이 옵션은 &lt;code&gt;Integer&lt;/code&gt;에만 적용되고 &lt;code&gt;Long&lt;/code&gt;이나 &lt;code&gt;Short&lt;/code&gt;에는 영향을 주지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;OpenJDK 구현에서는 &lt;code&gt;-Djava.lang.Integer.IntegerCache.high=&amp;lt;max&amp;gt;&lt;/code&gt; 로 지정해도 같은 효과가 나지만, 이 값은 &lt;code&gt;System.getProperty()&lt;/code&gt;로 조회하는 공개 시스템 프로퍼티가 아니라 VM이 별도로 보관하는 saved property입니다. 따라서 이 이름에 의존하기보다는 &lt;code&gt;-XX:AutoBoxCacheMax&lt;/code&gt; 옵션을 쓰는 편이 안전합니다. 두 값을 함께 지정하면 VM이 &lt;code&gt;-XX:AutoBoxCacheMax&lt;/code&gt; 값으로 이 프로퍼티를 덮어쓰므로 &lt;code&gt;-XX&lt;/code&gt; 옵션이 우선합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jdk_9부터_deprecated된_wrapper_생성자&quot;&gt;JDK 9부터 deprecated된 wrapper 생성자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;new Integer(int)&lt;/code&gt;에는 JDK 9부터 &lt;code&gt;@Deprecated&lt;/code&gt; 가 붙었습니다. 그전에는 &lt;code&gt;javac&lt;/code&gt;이 이 코드를 경고하지 않았습니다. &lt;a href=&quot;https://findbugs.sourceforge.net/&quot;&gt;FindBugs&lt;/a&gt; 같은 정적 분석기나 IntelliJ IDEA의 인스펙션을 써야 찾을 수 있었습니다. Eclipse에 FindBugs 플러그인을 설치하면 Problems 뷰에 아래와 같은 경고가 떴습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;M P Bx: Method study.Test.testInteger() invokes inefficient new Integer(int) constructor;
        use Integer.valueOf(int) instead&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금은 &lt;code&gt;javac&lt;/code&gt;이 알려 줍니다. &lt;code&gt;-Xlint:deprecation&lt;/code&gt; 옵션을 붙이면 위치까지 경고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;shell&quot;&gt;$ javac -Xlint:deprecation OldStyle.java
OldStyle.java:3: warning: [deprecation] Integer(int) in Integer has been deprecated
        Integer c = new Integer(1);
                    ^
OldStyle.java:4: warning: [deprecation] Double(double) in Double has been deprecated
        Double d = new Double(1.0);
                   ^
2 warnings&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDK 25의 소스에는 &lt;code&gt;@Deprecated(since=&quot;9&quot;)&lt;/code&gt; 가 붙어 있습니다. &lt;code&gt;forRemoval&lt;/code&gt; 이 없으므로 제거 예정 API는 아니지만, 새 코드에서 쓸 이유는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java/lang/Integer.java (JDK 25)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    /**
     * @deprecated
     * It is rarely appropriate to use this constructor. The static factory
     * {@link #valueOf(int)} is generally a better choice, as it is
     * likely to yield significantly better space and time performance.
     */
    @Deprecated(since=&quot;9&quot;)
    public Integer(int value) {&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FindBugs의 후속 프로젝트인 &lt;a href=&quot;https://spotbugs.github.io/&quot;&gt;SpotBugs&lt;/a&gt;에도 같은 규칙이 &lt;code&gt;DM_NUMBER_CTOR&lt;/code&gt; 로 남아 있습니다. 설명 문구는 FindBugs 시절과 거의 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Using &lt;code&gt;new Integer(int)&lt;/code&gt; is guaranteed to always result in a new object whereas &lt;code&gt;Integer.valueOf(int)&lt;/code&gt; allows caching of values to be done by the compiler, class library, or JVM. Using of cached values avoids object allocation and the code will be faster.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Values between -128 and 127 are guaranteed to have corresponding cached instances and using &lt;code&gt;valueOf&lt;/code&gt; is approximately 3.5 times faster than using constructor. For values outside the constant range the performance of both styles is the same.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Unless the class must be compatible with JVMs predating Java 5, use either autoboxing or the &lt;code&gt;valueOf()&lt;/code&gt; method when creating instances of &lt;code&gt;Long&lt;/code&gt;, &lt;code&gt;Integer&lt;/code&gt;, &lt;code&gt;Short&lt;/code&gt;, &lt;code&gt;Character&lt;/code&gt;, and &lt;code&gt;Byte&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; SpotBugs DM_NUMBER_CTOR: Method invokes inefficient Number constructor; use static valueOf instead
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다만 인용문의 &apos;약 3.5배&amp;#8217;라는 수치와 캐시 범위 밖에서는 성능이 같다는 설명은 Java API의 보장이 아닙니다. 실제 차이는 확장된 캐시 범위와 JIT 최적화, 실행 환경에 따라 달라질 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;IDE의 내장 검사로도 찾을 수 있습니다. Eclipse는 Java &amp;gt; Compiler &amp;gt; Errors/Warnings의 &apos;Deprecated and restricted API&apos; 아래 &apos;Deprecated API&apos; 항목의 기본값이 Warning이라 별도 플러그인 없이 deprecation 경고를 표시합니다. IntelliJ IDEA에는 &apos;Number constructor call with primitive argument&apos;(&lt;code&gt;CachedNumberConstructorCall&lt;/code&gt;) 인스펙션이 Java &amp;gt; Numeric issues 그룹에 있습니다. &lt;code&gt;Long&lt;/code&gt;, &lt;code&gt;Integer&lt;/code&gt;, &lt;code&gt;Short&lt;/code&gt;, &lt;code&gt;Byte&lt;/code&gt; 생성자에 primitive 값을 넘기는 코드를 찾아 &lt;code&gt;valueOf()&lt;/code&gt;로 바꾸도록 안내합니다. 기본값으로는 &apos;Report only when constructor is @Deprecated&apos; 옵션이 켜져 있어서, 생성자가 deprecated된 JDK 9 이상에서만 보고합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;박싱된_타입을_반복문에_쓸_때의_비용&quot;&gt;박싱된 타입을 반복문에 쓸 때의 비용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;박싱과 언박싱이 메서드 호출로 컴파일된다는 것을 알면, 반복문에서 박싱된 타입을 누적 변수로 쓸 때 어떤 일이 생기는지도 예측할 수 있습니다. 다음은 Effective Java 3판의 &apos;Item 6: 불필요한 객체 생성을 피하라&amp;#8217;에 나오는 예제를 이 글의 측정 코드에 맞게 변형한 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static long boxed() {
    Long sum = 0L;
    for (long i = 0; i &amp;lt; Integer.MAX_VALUE; i++) {
        sum += i;
    }
    return sum;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;sum += i&lt;/code&gt; 한 줄이 언박싱과 박싱을 모두 거치는 코드로 컴파일됩니다. 역컴파일하면 이 한 줄이 &lt;code&gt;longValue()&lt;/code&gt;와 &lt;code&gt;Long.valueOf()&lt;/code&gt; 호출로 바뀐 것을 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java -jar cfr-0.152.jar --sugarboxing false SumTest.class&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    static long boxed() {
        Long l = Long.valueOf((long)0L);
        for (long i = 0L; i &amp;lt; Integer.MAX_VALUE; ++i) {
            l = Long.valueOf((long)(l.longValue() + i));
        }
        return l.longValue();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;바이트코드로도 같은 흐름을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;  static long boxed();
    Code:
         0: lconst_0
         1: invokestatic  #7    // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
         4: astore_0
         5: lconst_0
         6: lstore_1
         7: lload_1
         8: ldc2_w        #15   // long 2147483647l
        11: lcmp
        12: ifge          32
        15: aload_0
        16: invokevirtual #17   // Method java/lang/Long.longValue:()J
        19: lload_1
        20: ladd
        21: invokestatic  #7    // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
        24: astore_0
        25: lload_1
        26: lconst_1
        27: ladd
        28: lstore_1
        29: goto          7&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반복할 때마다 &lt;code&gt;longValue()&lt;/code&gt;로 값을 꺼내 더한 뒤 &lt;code&gt;Long.valueOf()&lt;/code&gt;로 다시 박싱합니다. 누적값이 127을 넘은 뒤에는 &lt;code&gt;Long.valueOf()&lt;/code&gt;가 새 &lt;code&gt;Long&lt;/code&gt; 인스턴스를 반환합니다. 다만 실제 객체 할당은 JIT 최적화로 제거될 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;누적 변수의 타입만 &lt;code&gt;long&lt;/code&gt; 으로 바꾸면 박싱이 사라집니다. 두 방식을 한 번씩 재는 프로그램을 JDK 25에서 세 번 실행해 보면 약 10배 차이가 납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Long: 5122 ms, long: 507 ms
Long: 4987 ms, long: 516 ms
Long: 4919 ms, long: 513 ms&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JMH 같은 벤치마크 도구를 쓰지 않고 &lt;code&gt;System.nanoTime()&lt;/code&gt; 으로 각 실행에서 한 번씩 잰 값이라 절대 수치를 그대로 믿을 수는 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;캐시가_없는_double와_float&quot;&gt;캐시가 없는 Double와 Float&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정수 계열과 달리 &lt;code&gt;Double&lt;/code&gt;와 &lt;code&gt;Float&lt;/code&gt;에는 캐시가 없습니다. JLS 5.1.7도 &lt;code&gt;float&lt;/code&gt; 와 &lt;code&gt;double&lt;/code&gt; 에는 참조 동일성을 요구하지 않습니다. JDK 25의 &lt;code&gt;Double.valueOf()&lt;/code&gt;는 아직도 생성자를 그대로 호출합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;java/lang/Double.java (JDK 25)&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    public static Double valueOf(double d) {
        return new Double(d);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 JDK 25의 &lt;code&gt;Double.valueOf()&lt;/code&gt;는 값이 작아도 캐시된 인스턴스를 반환하지 않습니다. 정수 계열이라면 캐시되었을 -128~127 범위의 값도 박싱할 때마다 새 객체가 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jdk_버전별_변경_이력&quot;&gt;JDK 버전별 변경 이력&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;javac&lt;/code&gt;이 박싱과 언박싱을 &lt;code&gt;valueOf()&lt;/code&gt;와 &lt;code&gt;intValue()&lt;/code&gt; 호출로 컴파일하는 방식은 JDK 5 이후로 바뀌지 않았습니다. 대신 캐시의 구현과 생성자의 deprecation 상태, 그리고 언어 명세가 보장하는 범위가 아래와 같이 달라졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 80%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;JDK 버전&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;변경 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 5&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;auto boxing과 unboxing 도입. &lt;code&gt;valueOf(int)&lt;/code&gt; 등의 팩터리 메서드가 추가되고 -128~127을 캐시하기 시작&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 7&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;HotSpot의 &lt;code&gt;-XX:AutoBoxCacheMax&lt;/code&gt; 옵션이 넘기는 &lt;code&gt;java.lang.Integer.IntegerCache.high&lt;/code&gt; 값을 &lt;code&gt;IntegerCache&lt;/code&gt; 가 읽어 캐시 상한을 정하도록 변경 (JDK-6807702)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 9&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;wrapper 클래스의 생성자에 &lt;code&gt;@Deprecated(since=&quot;9&quot;)&lt;/code&gt; 부여. JLS 5.1.7의 참조 동일성 보장 조건이 SE 8의 &apos;리터럴&amp;#8217;에서 &apos;상수식의 평가 결과&amp;#8217;로 바뀌고, 대상 타입에 &lt;code&gt;long&lt;/code&gt; 포함&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 12&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;&lt;code&gt;IntegerCache&lt;/code&gt; 를 비롯한 박스 캐시를 CDS 아카이브에 저장하고, 아카이브를 사용할 수 있으면 실행 시점에 읽어 오도록 변경 (JDK-8209120, JDK-8213033)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 14&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JLS 5.1.7의 참조 동일성 보장 대상 타입 목록에 &lt;code&gt;byte&lt;/code&gt; 를 다시 포함 (SE 7에는 있었고 SE 8–13에는 빠져 있었음)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 16&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JEP 390으로 wrapper 클래스를 value-based class로 지정하고, 생성자를 &lt;code&gt;forRemoval = true&lt;/code&gt; 로 표시&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 22&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;박스 캐시 배열에 &lt;code&gt;@Stable&lt;/code&gt; 을 붙여 JIT이 상수로 접을 수 있게 함 (JDK-8295555)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;JDK 25&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;생성자의 &lt;code&gt;forRemoval&lt;/code&gt; 표시를 해제. 제거 예정이 아닌 일반 deprecated 상태로 되돌림 (JDK-8354335)&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 표의 JDK 버전은 OpenJDK mainline 최초 반영을 기준으로 합니다. &lt;code&gt;IntegerCache&lt;/code&gt;의 상한 설정(JDK-6807702)은 6u14에, CDS 아카이브 반영(JDK-8209120)은 11.0.6에, 박스 캐시의 &lt;code&gt;@Stable&lt;/code&gt; 변경(JDK-8295555)은 21.0.2에 backport되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;forRemoval&lt;/code&gt; 이 붙어 있던 JDK 16부터 JDK 24까지는 아무 옵션 없이 컴파일해도 removal 경고가 났습니다. JDK 21과 JDK 25에서 같은 코드를 컴파일해 보면 차이가 드러납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;shell&quot;&gt;$ ~/.sdkman/candidates/java/21.0.8-tem/bin/javac OldStyle.java
OldStyle.java:3: warning: [removal] Integer(int) in Integer has been deprecated and marked for removal
        Integer c = new Integer(1);
                    ^
OldStyle.java:4: warning: [removal] Double(double) in Double has been deprecated and marked for removal
        Double d = new Double(1.0);
                   ^
2 warnings

$ ~/.sdkman/candidates/java/25-tem/bin/javac OldStyle.java
Note: OldStyle.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JLS 5.1.7의 문구도 버전마다 달랐습니다. JLS 3판(Java 5·6)과 Java SE 7은 리터럴 여부와 관계없이 &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;byte&lt;/code&gt; 값, &lt;code&gt;&apos;\u0000&apos;&lt;/code&gt; 부터 &lt;code&gt;&apos;\u007f&apos;&lt;/code&gt; 까지의 &lt;code&gt;char&lt;/code&gt; 값, -128부터 127까지의 &lt;code&gt;int&lt;/code&gt;·&lt;code&gt;short&lt;/code&gt; 값을 박싱한 결과가 같은 참조라고 규정했습니다. Java SE 8은 이 조건을 &apos;리터럴&amp;#8217;로 좁혀 적었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If the value &lt;code&gt;p&lt;/code&gt; being boxed is an integer literal of type &lt;code&gt;int&lt;/code&gt; between &lt;code&gt;-128&lt;/code&gt; and &lt;code&gt;127&lt;/code&gt; inclusive, or the boolean literal &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, or a character literal between &lt;code&gt;&apos;\u0000&apos;&lt;/code&gt; and &lt;code&gt;&apos;\u007f&apos;&lt;/code&gt; inclusive, then let &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; be the results of any two boxing conversions of &lt;code&gt;p&lt;/code&gt;. It is always the case that &lt;code&gt;a == b&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;attribution&quot;&gt;
&amp;#8212; JLS SE8 5.1.7
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java SE 9는 조건을 상수식의 평가 결과로 바꾸고 &lt;code&gt;long&lt;/code&gt; 을 대상 타입에 넣었습니다. &lt;code&gt;Integer x = 100 + 27;&lt;/code&gt; 처럼 리터럴이 아닌 상수식도 SE 8과 달리 보장 대상이 됩니다. JDK 구현은 JDK 5부터 &lt;code&gt;valueOf()&lt;/code&gt;에서 -128~127을 캐시해 왔으므로, 어느 판의 문구에서도 실제 동작은 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK의 &lt;code&gt;javac&lt;/code&gt;은 auto boxing을 &lt;code&gt;valueOf()&lt;/code&gt;, unboxing을 &lt;code&gt;intValue()&lt;/code&gt; 계열의 메서드 호출로 컴파일합니다. 전용 바이트코드 명령은 없습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;Integer.valueOf()&lt;/code&gt;는 -128~127을 항상 캐시하고, JLS 5.1.7은 이 범위에 속하는 상수식의 박싱 결과가 같은 참조여야 한다고 요구합니다. &lt;code&gt;Integer&lt;/code&gt;에 한해 &lt;code&gt;-XX:AutoBoxCacheMax&lt;/code&gt; 로 캐시 상한을 넓힐 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;박싱된 타입끼리는 &lt;code&gt;==&lt;/code&gt; 가 아니라 &lt;code&gt;equals()&lt;/code&gt;로 비교해야 합니다. &lt;code&gt;==&lt;/code&gt; 의 결과가 값의 크기와 실행 옵션에 따라 달라집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wrapper 클래스의 생성자는 JDK 9부터 deprecated 상태입니다. JDK 8까지는 &lt;code&gt;javac&lt;/code&gt;이 경고하지 않아 FindBugs나 IDE 검사에 기대야 했지만, 지금은 &lt;code&gt;javac&lt;/code&gt;이 deprecation을 알리고 Eclipse와 IntelliJ IDEA도 기본 설정으로 경고합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;되도록 primitive 타입을 쓰고, &lt;code&gt;Collection&lt;/code&gt;의 요소나 generics의 타입 인자처럼 객체가 필요한 자리에만 박싱된 타입을 씁니다. (Effective Java 3판, &apos;Item 61: 박싱된 기본 타입보다는 기본 타입을 사용하라&apos;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;주요 변경이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2026.09.26&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JLS 5.1.7 문구의 판본별 변화(3판·SE 7 → SE 8 리터럴 → SE 9 상수식 → SE 14 &lt;code&gt;byte&lt;/code&gt;)를 바로잡음&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JDK-6807702의 설명을 라이브러리 변경으로 고치고 6u14 backport 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:AutoBoxCacheMax&lt;/code&gt; 의 C2 의존성과 &lt;code&gt;-D&lt;/code&gt; 프로퍼티와의 우선순위, IntelliJ 인스펙션 기본 옵션 보완&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2026.08.29&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JDK 25 기준으로 바이트코드, &lt;code&gt;IntegerCache&lt;/code&gt; 소스, 경고 메시지를 다시 확인해 갱신&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Eclipse 캡처 화면을 예제 소스와 &lt;code&gt;javap&lt;/code&gt; 출력으로 교체하고, 화면에 있던 FindBugs 경고 메시지는 본문에 인용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;-XX:AutoBoxCacheMax&lt;/code&gt; 옵션, JLS 5.1.7 인용, 실행 시간 측정 결과 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FindBugs 항목을 SpotBugs 기준으로 갱신하고, Eclipse·IntelliJ IDEA의 현재 경고 방식 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Effective Java 인용을 2판(Item 5, 49)에서 3판(Item 6, 61) 기준으로 수정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&apos;JDK 버전별 변경 이력&apos; 절을 추가하고, 2009년 원문을 언급하던 서술을 버전 기준 서술로 정리&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;제목을 &apos;Java의 auto boxing과 unboxing은 어떻게 컴파일될까?&apos;에서 &apos;Java auto boxing의 컴파일 방식과 Integer 캐시&amp;#8217;로 변경&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;원문에 있던 역컴파일 결과를 CFR 0.152로 다시 뽑아 수록. 역컴파일러가 박싱을 도로 접는다는 점과 &lt;code&gt;--sugarboxing&lt;/code&gt; 옵션 설명 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JLS와 &lt;code&gt;Integer.valueOf()&lt;/code&gt;가 보장하는 범위를 구분하고 CDS, 확장 캐시, JIT 및 backport 관련 표현을 보완&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2009.02.10&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;최초 작성&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고_자료&quot;&gt;참고 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html#jls-5.1.7&quot;&gt;JLS 5.1.7: Boxing Conversion&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html&quot;&gt;Integer javadoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/Integer.java&quot;&gt;JDK 25 Integer.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/lang/Double.java&quot;&gt;JDK 25 Double.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.benf.org/other/cfr/&quot;&gt;CFR: another Java decompiler&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://spotbugs.readthedocs.io/en/stable/bugDescriptions.html&quot;&gt;SpotBugs Bug descriptions&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/help/inspectopedia/CachedNumberConstructorCall.html&quot;&gt;IntelliJ IDEA Inspectopedia: Number constructor call with primitive argument&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://help.eclipse.org/latest/topic/org.eclipse.jdt.doc.user/reference/preferences/java/compiler/ref-preferences-errors-warnings.htm&quot;&gt;Eclipse JDT: Java Compiler Errors/Warnings Preferences&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.7&quot;&gt;JLS SE8 5.1.7: Boxing Conversion&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/390&quot;&gt;JEP 390: Warnings for Value-Based Classes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-6807702&quot;&gt;JDK-6807702: Integer.valueOf cache should be configurable&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8209120&quot;&gt;JDK-8209120: Archive the Integer.IntegerCache&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8213033&quot;&gt;JDK-8213033: Archive remaining primitive box caches&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8295555&quot;&gt;JDK-8295555: Primitive wrapper caches could be @Stable&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.openjdk.org/browse/JDK-8354335&quot;&gt;JDK-8354335: No longer deprecate wrapper class constructors for removal&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/effective-java/9780134686097/&quot;&gt;Effective Java, Third Edition&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring MVC 2.5을 활용한 파일업로드</title>
      <link>https://blog.benelog.net//2228221.html</link>
      <pubDate>Sun, 8 Feb 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2228221.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;간단한 파일 업로드 기능을 만들어야 할 일이 생겨서, Spring 2.5의 annotation을 이용한 Action에서 이를 처리하게 했습니다. 실무에서 썼던 것을 더 단순한 예제로 재구성해서 정리해봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Maven의 pom.xml에 파일업로드 기능에서 참조하는 commons-fileupload 라이브러리에 대한 dependency를 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;commons-fileupload&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;commons-fileupload&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;1.2.1&amp;lt;/version&amp;gt;

&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;web.xml에는 applicationContext 파일의 위치를 지정하고, *.do를 스프링에서 처리하도록 설정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;servlet&amp;gt;
   &amp;lt;servlet-name&amp;gt;dispatcher&amp;lt;/servlet-name&amp;gt;
   &amp;lt;servlet-class&amp;gt;org.springframework.web.servlet.DispatcherServlet&amp;lt;/servlet-class&amp;gt;
   &amp;lt;init-param&amp;gt;
     &amp;lt;param-name&amp;gt;contextConfigLocation&amp;lt;/param-name&amp;gt;
     &amp;lt;param-value&amp;gt;classpath:applicationContext.xml&amp;lt;/param-value&amp;gt;
   &amp;lt;/init-param&amp;gt;
   &amp;lt;/servlet&amp;gt;
   &amp;lt;servlet-mapping&amp;gt;
     &amp;lt;servlet-name&amp;gt;dispatcher&amp;lt;/servlet-name&amp;gt;
     &amp;lt;url-pattern&amp;gt;*.do&amp;lt;/url-pattern&amp;gt;
   &amp;lt;/servlet-mapping&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;업로드 기능을 간단히 테스트할 수 있는 jsp페이지를 만들어봅니다. 단순히 파일 1개를 &quot;file&quot;이라는 변수명으로 업로드 요청을 하는 페이지입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;jsp&quot;&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;spring&quot; uri=&quot;http://www.springframework.org/tags&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;form&quot; uri=&quot;http://www.springframework.org/tags/form&quot;%&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;업로드 테스트&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;form action=&quot;/study/upload.do&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
&amp;lt;p&amp;gt;
    &amp;lt;label for=&quot;file&quot;&amp;gt;파일1 &amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;file&quot; name=&quot;file&quot;&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;
    &amp;lt;input type=&quot;submit&quot; value=&quot;전송&quot;/&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 applicationContext파일을 아래와 같이 선언합니다. &lt;code&gt;DefaultAnnotationHandlerMapping&lt;/code&gt; 을 이용해서 annotation을 이용한 Controller 설정을가능하게 합니다. &lt;code&gt;&amp;lt;context:component-scan/&amp;gt;&lt;/code&gt; 태그를 사용해서, 설정을 scan할 패키지를지정합니다. 그리고, 파일이 저장될 디렉터리는 &lt;code&gt;${repository.path}&lt;/code&gt; 속성으로 표시했습니다.
Maven의 resource filter기능을 이용해서 실행환경에 따라 다른 값을 넣게 하면 편리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd&quot;&amp;gt;

    &amp;lt;bean  class=&quot;org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping&quot;&amp;gt;
        &amp;lt;property name=&quot;alwaysUseFullPath&quot; value=&quot;true&quot;/&amp;gt;
    &amp;lt;/bean&amp;gt;
    &amp;lt;bean id=&quot;multipartResolver&quot;
        class=&quot;org.springframework.web.multipart.commons.CommonsMultipartResolver&quot;/&amp;gt;

    &amp;lt;bean id=&quot;respository&quot; class=&quot;study.repository.FileRepository&quot;&amp;gt;
        &amp;lt;constructor-arg value=&quot;${repository.path}&quot; /&amp;gt;
    &amp;lt;/bean&amp;gt;
    &amp;lt;context:component-scan    base-package=&quot;study.action&quot;/&amp;gt;
&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;실제적인 파일의 저장기능을 담당하는 클래스인 FileRepository에서는 간단하게 UUID를 이용해서 키를 생성하고 path필드로 지정된 디렉터리에 저장을 해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package study.repository;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

import org.springframework.web.multipart.MultipartFile;

public class FileRepository {
    private String path;
    public FileRepository(String path) {
        this.path = path;
        File saveFolder = new File(path);
        if(!saveFolder.exists() || saveFolder.isFile()){
            saveFolder.mkdirs();
        }
    }

    public String saveFile(MultipartFile sourcefile) throws IOException{
        if ((sourcefile==null)||(sourcefile.isEmpty())) return null;
        String key = UUID.randomUUID().toString();
        String targetFilePath = path+&quot;/&quot;+ key;
        sourcefile.transferTo(new File(targetFilePath));
        return key;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;추가적으로 키값을 넣어주면 파일을 반환해주는 메서드나, 파일의 종류나 날짜에 따라서 하위 디렉터리를 구분해서 생성하는 기능도 넣을 수있을 것입니다. 그리고 더 확장한다면, 따로 주요 메서드를 선언한 Repository 라는 인터페이스를 정의하고, DB를저장소로 활용하는 DbRepository 와 같이 이름 붙인 구현 클래스도 만들어 볼 수 있겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 @Controller , @RequestMapping, @RequestParam, @Autowired의 Anntation을활용해서 Controller 클래스를 작성합니다. 업로드 후 화면에 키값만 뿌도록 해서 java.io.Writer클래스를 화면출력을 위해 사용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package study.action;

import java.io.IOException;
import java.io.Writer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import study.repository.FileRepository;

@Controller
public class FileAction {
     private FileRepository respository;

    @Autowired
    public void setRespository(FileRepository respository) {
        this.respository = respository;
    }
    @RequestMapping(&quot;/upload.do&quot;)
    public void execute(@RequestParam(&quot;file&quot;) MultipartFile file,
            Writer out) throws IOException{
        String key = respository.saveFile(file);
        out.write(key);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;MultipartFile 클래스를 파라미터로 받는 메서드는 &lt;a href=&quot;http://static.springsource.org/spring/docs/3.0.0.M1/javadoc-api/org/springframework/mock/web/MockMultipartFile.html&quot;&gt;MockMultiPartFile&lt;/a&gt;를 이용해서 테스트하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;참고로 &lt;a href=&quot;http://chanwook.tistory.com/758&quot;&gt;최근 로드존슨이 인터뷰에서 한 말&lt;/a&gt;에 따르면 기존 Spring MVC의 Controller interface는 삭제될 것이라고 하네요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;spring_mvc_2_5_관련_자료&quot;&gt;Spring MVC 2.5 관련 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.infoq.com/articles/spring-2.5-ii-spring-mvc&quot; class=&quot;bare&quot;&gt;http://www.infoq.com/articles/spring-2.5-ii-spring-mvc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://chanwook.tistory.com/576&quot;&gt;스프링 2.5 애너테이션 기반 MVC 예제&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://springtutorial.googlecode.com/svn/trunk/moimApp/src/spring/tutorial/web/MoimController.java&quot;&gt;http://springtutorial.googlecode.com/svn/trunk/moimApp/src/spring/tutorial/web/MoimController.java&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://steelheart.kr/tc/173&quot;&gt;Spring 2.5 Annotation기반 Controller 끄적거림&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://steelheart.kr/tc/entry/using-web-argument-resolver?category=0&quot;&gt;Annotation기반 컨트롤러에 custom argument 적용하기 (WebArgumentResolver)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://chanwook.tistory.com/548&quot;&gt;Annotated Spring MVC Controller&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://corund.egloos.com/1815311&quot;&gt;Spring Framework 2.5의 Annotation based Controller의 메서드 파라미터에서 주의점&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>EMMA + Eclipse + Maven2 + Hudson</title>
      <link>https://blog.benelog.net//2212119.html</link>
      <pubDate>Fri, 23 Jan 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2212119.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://emma.sourceforge.net/&quot;&gt;EMMA&lt;/a&gt;는 테스트코드가 검증해 주는 코드영역에 대해서 보고해 주는 도구입니다. 전체 코드 중 몇 %가 테스트 코드를 거쳐가고 있는지 쉽게 수치를 낼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;EMMA의  Eclipse plugin인 EclEmma는 &lt;a href=&quot;http://update.eclemma.org/를&quot; class=&quot;bare&quot;&gt;http://update.eclemma.org/를&lt;/a&gt; plugin의 update site로 지정하면 설치할 수 있습니다.  설치 후 프로젝트에서 우클릭-Coverage As - JUnit Test 메뉴를 선택하면 전체 Junit Test를 실행하고, Test Coverage에 대한 보고서를 생성해 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/emma/eclipse-emma-menu.jpg&quot; alt=&quot;eclipse-emma-menu.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;EMMA의 Maven2 plugin인 &lt;a href=&quot;http://mojo.codehaus.org/emma-maven-plugin/&quot;&gt;Emma Maven Plugin Maven&lt;/a&gt;을  pom.xml파일의 reporting 선언에 아래와 같은 설정을 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;reporting&amp;gt;
  &amp;lt;plugins&amp;gt;
   ...
    &amp;lt;plugin&amp;gt;
      &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;emma-maven-plugin&amp;lt;/artifactId&amp;gt;
      &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
&amp;lt;/reporting&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://hudson.gotdns.com/wiki/display/HUDSON/Emma+Plugin%20&quot;&gt;Hudson의 위키에 있는 Emma plugin 설명페이지&lt;/a&gt;나 &lt;a href=&quot;http://mojo.codehaus.org/emma-maven-plugin/usage.html&quot;&gt;Maven의 emma-plugin 설명페이지&lt;/a&gt;를 보면 build절에도 plugin 설정을 추가하라고 되어있는데, 그렇게 하지 않아도 잘  실행이 되었습니다.
오히려 그렇게 추가를 하니, mvn test site처럼 test와 site phrase가 같이 돌아갈 때 아래와 같은 에러가 발생했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;java.lang.IllegalStateException: class [......] appears to be instrumented already&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Emma plugin 설정을 reporting 부분에 넣으면 테스트가 실행되므로 일부러 test phrase를 넣어줄 필요는 없습니다. 즉 이 때는 mvn site만 돌려주시면 테스트 결과와 Coverage Report를 모두 얻을 수 있습니다.
Emma plugin이 있을 때 &apos;mvn test site&amp;#8217;로 실행시켜서 테스트가 두번 돌면 Hudson에서 보고하는 테스트 개수도 2번씩 중복 집계가 되므로 더 혼동만 줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://mojo.codehaus.org/emma-maven-plugin/usage.html&quot;&gt;Maven의 emma-plugin 설명페이지&lt;/a&gt;에 따르면,  maven-surefire-plugin 설정에서 별도의 JVM으로 테스트를 실행시키위해 forkMode를 always로 설정하는 것은 중요하다고 합니다.
EMMA가 JVM의 종료 때도 기록을 하기 때문에 그렇다는군요. 다음과 같이 설정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;plugin&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;maven-surefire-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;inherited&amp;gt;true&amp;lt;/inherited&amp;gt;
        &amp;lt;configuration&amp;gt;
          &amp;lt;forkMode&amp;gt;always&amp;lt;/forkMode&amp;gt;
          &amp;lt;reportFormat&amp;gt;xml&amp;lt;/reportFormat&amp;gt;
        &amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 Maven을 통해 생성한 리포트를  Huson으로 보기 위해서는 &lt;strong&gt;Manage Hudson&amp;gt;Manage Plugins&lt;/strong&gt; 메뉴에서 Hudson Emma plugin을 설치하고 Hudson을 재시작합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;설치가 잘 되었다면, 프로젝트의 Configure 메뉴에서 EMMA의 보고서에 대해서 설정할 수 있는 항목이 생깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/emma/hudson-emma-config.jpg&quot; alt=&quot;hudson-emma-config.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기에서 Emma XML report 항목에 workspace를 기준으로 한 상대적인 경로로 coverage.xml의 위치를 지정해줍니다.
&lt;a href=&quot;http://hudson.gotdns.com/wiki/display/HUDSON/Emma+Plugin%20&quot;&gt;Hudson의 위키에 있는 Emma plugin 설명페이지&lt;/a&gt;에 보면 이 항목이 target/site/emma/coverage.xml으로 되어 있어서 혼동이 오기 쉬운데, trunk와 같은 Local module directory를 반드시 포함시켜줘야 합니다.
보통 SVN을 쓸 때 프로젝트 설정에서 별도로 지정을 하지 않으면 trunk같은 SVN의 path의 마지막 디렉터리가 Local module directory로 지정됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/emma/hudson-svn-config.jpg&quot; alt=&quot;hudson-svn-config.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Emma 보고서 설정란에서 the work space root라는 링크를 눌러도 금방 확인 할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위와 같이 설정된 프로젝트를 build하면 빌드의 맨 끝에 Recording Emma reports trunk/target/site/emma/coverage.xml 와 같은 메시지가 Console out 화면에서 뜰 것입니다.  그렇게 생성된 보고서는 &quot;프로젝트명&amp;gt;빌드번호&amp;gt;Coverage Report&quot; 메뉴에서 보실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/emma/hudson-emma-menu.jpg&quot; alt=&quot;hudson-emma-menu.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/emma/hudson-emma-report.jpg&quot; alt=&quot;hudson-emma-report.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;만약 Hudson에서 Emma를 실행한 빌드번호의 메뉴에서 &apos;Coverage Report&amp;#8217;라는 메뉴가 보이지 않는다면 &lt;a href=&quot;http://blog.benelog.net/2208375&quot;&gt;Hudson plugin 수동으로 빌드&amp;amp;업로드&lt;/a&gt;를 참조해서 최신 버전으로 플러그인을 업데이트 해보시기 바랍니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>JDepend + Eclipse + Maven2</title>
      <link>https://blog.benelog.net//2208368.html</link>
      <pubDate>Mon, 19 Jan 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2208368.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://clarkware.com/software/JDepend.html&quot;&gt;JDepend&lt;/a&gt;는 Java패키지간의 의존성에 대한 수치들을 알려 주는 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDepend의 Eclipse의 Plugin은 &lt;a href=&quot;http://andrei.gmxhome.de/eclipse/를&quot; class=&quot;bare&quot;&gt;http://andrei.gmxhome.de/eclipse/를&lt;/a&gt; Update site에 추가하면 설치할 수 있습니다. 다음의 링크들에서 보다 자세한 내용을 참조할 수 있습니다. 분석을 하고자 하는 소스폴더 위에서 우클릭을 한 후 &apos;Run JDepend Analysis&apos; 메뉴를 선택하면 의존성 분석 결과가 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jdepend/eclipse-jdepend.jpg&quot; alt=&quot;eclipse-jdepend.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이를 Maven을 통해서 생성하는 &lt;a href=&quot;http://mojo.codehaus.org/jdepend-maven-plugin/&quot;&gt;jdepend-maven-plugin&lt;/a&gt; 은 pom.xml에 아래와 같이 추가할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;reporting&amp;gt;

.....

&amp;lt;plugin&amp;gt;
  &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;jdepend-maven-plugin&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;2.0-beta-2&amp;lt;/version&amp;gt;
&amp;lt;/plugin&amp;gt;

&amp;lt;/reporting&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;mvn jdepend:generate&lt;/code&gt; 또는 &lt;code&gt;mvn site&lt;/code&gt; 명령을 통해서 보고서가 생성됩니다. mvn site로 실행했다면 Hudson의 프로젝트 홈에서 Maven Generated Site 메뉴를 통해서도 확인할 수 있습니다.
&lt;a href=&quot;http://mojo.codehaus.org/jdepend-maven-plugin/examples/jdepend-report.html&quot;&gt;샘플페이지&lt;/a&gt; 에 생성된 보고서의 형식이 나와있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jdepend/jdepend-report.jpg&quot; alt=&quot;jdepend-report.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://clarkware.com/software/JDepend.html&quot;&gt;JDepend 첫페이지&lt;/a&gt;나 생성된 보고서 안에서도 위의 요약 테이블에  수치들에 대한 설명이 잘 나와있습니다. 간단히 요약해서 정리하면,&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;TC (Total Classes) : 전체 클래스 수. CC + AC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CC (Concrete Classes) : Inteface나 추상클래스가 아닌 구상 클래스 수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AC (Abstract Classes) : Interface나 Abstract Class로 선언된 클래스 수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ca (&lt;span id=&quot;intelliTxt&quot;&gt;Afferent Couplings&lt;/span&gt;) : 이 패키지를 의존하고 있는 다른 패키지의 수. 이 패키지의 책임감을 나타내는 지표&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ce (&lt;span id=&quot;intelliTxt&quot;&gt;Efferent Couplings&lt;/span&gt;) : 이 패키지가 의존하고 있는 클래스가 있는 다른 패키지의 수. 이 패키지의 독립성을 나타내는 지표.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A (&lt;span id=&quot;intelliTxt&quot;&gt;Abstractness&lt;/span&gt;) : 총 클래스 갯수 중 인터페이스나 추상클래스의 비율. 1이라면 해당 패키지는 추상클래스나 인터페이스 밖에 없는 것.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I (&lt;span id=&quot;intelliTxt&quot;&gt;Instability&lt;/span&gt;) :   총 결합도 중 이 패키지의 외부의존성의 비율 (Ce / (Ce + Ca)). 변화에 대한 내성을 나타내는 지표. I=0이라면 완전하게 안정적인 것.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;D &lt;span id=&quot;intelliTxt&quot;&gt;(Distance from Main Sequence&lt;/span&gt;) :  이상적인 균형의 상태인  A + I = 1 의 함수에서 수직으로 떨어진 거리. (아래 그래프 참조)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://www.onjava.com/onjava/2004/01/21/graphics/figure3.gif&quot; alt=&quot;onjava/2004/01/21/graphics/figure3.gif&quot; title=&quot;onjava/2004/01/21/graphics/figure3.gif&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미지 출저 : &lt;a href=&quot;http://www.onjava.com/pub/a/onjava/2004/01/21/jdepend.html&quot;&gt;Managing Your Dependencies with JDepend&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;가장 중요한 것은 패키지 간의 순환참조를 보여주는 Cycles 부분입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/jdepend/jdepend-cycles.jpg&quot; alt=&quot;jdepend-cycles.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;순환참조 관계의 패키지들은 부분적으로 배포될 수도 없고, 한 패키지를 변경할 때 그 영향력을 파악하기도 힘들게 만듭니다. 순환 참조에 대한 자세한 내용은 아래의 링크를 참조하시기 바랍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://toby.epril.com/?p=263&quot;&gt;Code Organization &amp;amp; Cyclic Dependency Problem&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.kirkk.com/index.php?itemid=30&quot;&gt;Cyclic Dependencies&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.onjava.com/pub/a/onjava/2004/01/21/jdepend.html&quot;&gt;Managing Your Dependencies with JDepend&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.tistory.com/1767&quot;&gt;Code Organization Guidelines for Large Code Bases - 유겐 휄러&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;관련자료&quot;&gt;관련자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://younghoe.info/50&quot;&gt;JDepend에서 지원하는 설계 품질 측정치(design quality metrics)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.powerjava.net/blog/soulcarta/entry/Eclipse-Plugin-JDepend%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B2%80%EC%82%AC&quot;&gt;[Eclipse Plugin&lt;/a&gt; JDepend를 이용한 의존성 검사 개발관련]&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.tistory.com/967&quot;&gt;Eclipse에서 JDepend 사용하기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>JavaNCSS + Maven2 + Hudson</title>
      <link>https://blog.benelog.net//2204823.html</link>
      <pubDate>Fri, 16 Jan 2009 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2204823.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JavaNCSS를 Maven2을 통해 실행하고, Hudson을 통해 리포트를 확인하는 과정을 정리했습니다. &lt;a href=&quot;http://www.kclee.de/clemens/java/javancss/&quot;&gt;JavaNCSS&lt;/a&gt;는 주석이 아닌 라인수(Non Commenting Source Statements,NCSS)와 순환복잡도 수 (Cyclomatic Comlexity Number, CCN) 등을 측정해 주는 도구입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JavanNCSS에 관한 eclipse 플러그인도 찾을 수는 있었지만(&lt;a href=&quot;http://sourceforge.net/projects/jncss4eclipse/&quot; class=&quot;bare&quot;&gt;http://sourceforge.net/projects/jncss4eclipse/&lt;/a&gt; ) 가장 최신버전이 2002년에 올라온 것이라서 적용하지 않기로 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고, JavaNCSS에 대한 Maven의 플러그인도  &lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/&quot;&gt;javancss-maven-plugin&lt;/a&gt;과 &lt;a href=&quot;http://maven-plugins.sourceforge.net/maven-javancss-plugin/&quot;&gt;maven-javancss-plugin&lt;/a&gt;의 2가지가 존재했는데, &lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/&quot;&gt;javancss-maven-plugin&lt;/a&gt; 쪽이 보다 문서화가 잘 되어 있어서 그 쪽을 선택했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;pom.xml&lt;/code&gt; 에 아래 선언을 추가합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;reporting&amp;gt;

...

   &amp;lt;plugin&amp;gt;

     &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
     &amp;lt;artifactId&amp;gt;javancss-maven-plugin&amp;lt;/artifactId&amp;gt;
     &amp;lt;version&amp;gt;2.0-beta-2&amp;lt;/version&amp;gt;
      &amp;lt;configuration&amp;gt;
      &amp;lt;forceEncoding&amp;gt;utf-8&amp;lt;/forceEncoding&amp;gt;
       &amp;lt;/configuration&amp;gt;
    &amp;lt;/plugin&amp;gt;
  &amp;lt;/plugins&amp;gt;
&amp;lt;/reporting&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고는 &lt;code&gt;mvn javancss:report javancss:check&lt;/code&gt; 로 플러그인을 실행시켜 봅니다. 각각의 골에 대한 설명은 아래 페이지에서 보시면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/report-mojo.html&quot;&gt;javancss:report&lt;/a&gt; : 리포트 생성&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/check-mojo.html&quot;&gt;javancss:check&lt;/a&gt; : CCN이나 NCSS 값이 너무 크면 빌드를 실패시킴&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제외하거나 추가할 파일을 지정하려면 아래의 configuration 태그 아래에 includes, excludes 태그로 지정하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;configuration&amp;gt;

.....
  &amp;lt;includes&amp;gt;
    &amp;lt;include&amp;gt;**/*foo.java&amp;lt;/include&amp;gt;
  &amp;lt;/includes&amp;gt;
  &amp;lt;excludes&amp;gt;
    &amp;lt;exclude&amp;gt;**/bar.java&amp;lt;/exclude&amp;gt;
  &amp;lt;exclude&amp;gt;**/foobar.java&amp;lt;/exclude&amp;gt;
  &amp;lt;/excludes&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CCN이나 NCSS값의 한계 허용치는 아래와 같이 지정합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;configuration&amp;gt;
....
  &amp;lt;failOnViolation&amp;gt;true&amp;lt;/failOnViolation&amp;gt;
  &amp;lt;ccnLimit&amp;gt;12&amp;lt;/ccnLimit&amp;gt;
  &amp;lt;ncssLimit&amp;gt;90&amp;lt;/ncssLimit&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한가지 주의할 점은, 2.0-beta-2 버전을 실행할 경우, 메서드 내부에 annotation선언이 있다면 아래와 같은 에러메시지를 받게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;Encountered &quot;@ SuppressWarnings ( \&quot;unused\&quot; ) int notUsedVariable =&quot; at line xxx, column yyy.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 것은   &lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/&quot;&gt;javancss-maven-plugin&lt;/a&gt;  버전 2.0-beta-2가 이 의존하고 있는 JavaNCSS 버전 29.49 에서 가지고 있는 버그입니다. (&lt;a href=&quot;http://jira.codehaus.org/browse/MJNCSS-16&quot; class=&quot;bare&quot;&gt;http://jira.codehaus.org/browse/MJNCSS-16&lt;/a&gt; 참조) JavaNCSS 버전 29.50에서는 해결된 문제이고, 이 플러그인의 최신 버전인 &lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/&quot;&gt;javancss-maven-plugin&lt;/a&gt; 2.0-beta-3버전에서는 이런 문제가 발생하지 않는다고 합니다. 그런데, codehause의 Maven Repository를 뒤져보니 (&lt;a href=&quot;http://repository.codehaus.org/org/codehaus/mojo/javancss-maven-plugin/&quot; class=&quot;bare&quot;&gt;http://repository.codehaus.org/org/codehaus/mojo/javancss-maven-plugin/&lt;/a&gt;) 아직 2.0-beta-3버전은 올라와 있지 않습니다. snapshop 버전을 찾아서 쓸 수도 있겠지만, 그것보다는plugin 설정에서 dependency설정을 덮어쓰는 편이 더 간편하게 느껴졌습니다. &lt;code&gt;pom.xml&lt;/code&gt; 의 build절에 아래와같이 추가하면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;build&amp;gt;

   &amp;lt;plugin&amp;gt;
     &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;javancss-maven-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.0-beta-2&amp;lt;/version&amp;gt;
        &amp;lt;dependencies&amp;gt;
          &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;javancss&amp;lt;/groupId&amp;gt;
              &amp;lt;artifactId&amp;gt;javancss&amp;lt;/artifactId&amp;gt;
              &amp;lt;version&amp;gt;29.50&amp;lt;/version&amp;gt;
             &amp;lt;/dependency&amp;gt;
         &amp;lt;/dependencies&amp;gt;
     &amp;lt;/plugin&amp;gt;
     &amp;lt;/plugins&amp;gt;
  &amp;lt;/build&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;생성된 리포트를 보기 위해서, Hudson에서는 프로젝트에서 Configure 설정에서 Publish Java NCSS report를 선택을 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/javancss/hudson-javancss-config.jpg&quot; alt=&quot;hudson-javancss-config.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고는 &lt;a href=&quot;http://mojo.codehaus.org/javancss-maven-plugin/report-mojo.html&quot;&gt;javancss:report&lt;/a&gt;  Goal이 포함된 빌드를 실행시켰다면 프로젝트 홈 화면에서 Java NCSS Report라는 링크가 생기게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/javancss/hudson-javancss-menu.jpg&quot; alt=&quot;hudson-javancss-menu.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;클릭을 하면 생성된 리포트를 볼 수가 있죠. (패키지명은 제가 지웠습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/javancss/hudson-javancss-report.jpg&quot; alt=&quot;hudson-javancss-report.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Maven에서 mvn site명령으로 site 절(phrase)까지 들어갔다면 리포트를 모은 페이지를 따로 생성해 주기도합니다. Hudson의 프로젝트 메인화면에서 Maven-generated site 링크를 통해서 보실 수 있습니다.Hudson에서 생성해주는 페이지가 더 이쁘기는하지만, JavaNCSS의 보고서는 Maven-generated site가 더많은 정보를 제공합니다. 예를 들면 가장 주석을 제외한 라인수(NCSS)가 많은 30위까지의 클래스들, 메서드들 같은 순위등같은 것들을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/javancss/mvn-site-reports.jpg&quot; alt=&quot;mvn-site-reports.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>PMD + Eclipse + Maven2 + Hudson</title>
      <link>https://blog.benelog.net//2176171.html</link>
      <pubDate>Fri, 19 Dec 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2176171.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코드 검사도구인 &lt;a href=&quot;http://pmd.sourceforge.net/&quot;&gt;PMD&lt;/a&gt; 를 Eclipse plugin을 설정하고, Maven을 통해서도 같은 규칙으로 코드를 검사한 보고서를 생성하고, Hudson을 통해서 확인하는 과정을 정리해 봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eclipse에서는 update site를 &lt;a href=&quot;http://pmd.sf.net/eclipse&quot; class=&quot;bare&quot;&gt;http://pmd.sf.net/eclipse&lt;/a&gt; 로 지정해서 플러그인을 설치합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eclipse 메뉴의 Window- Preferences를 가면 Rule설정 파일을 export, import할 수 있는 기능이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/pmd/eclipse-pmd-rule.jpg&quot; alt=&quot;eclipse-pmd-rule.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Project의 Properties에도 PMD관련 설정이 있습니다. 외부에서 만든 Rule파일을 바로 참조해도 되고, 여기서 설정된 것을파일로 생성할 수도 있습니다. 이미 있는 프로젝트에서 Rule를 설정할 때는 Project의 Properties에서 Rule들을고른 후에 생기는 warning이나 error를 보고 warning이 안 뜨게 소스를 고치거나 Rule을 제외한 후, 최종결정Rule들로 Ruleset 정의 파일을 생성하는 것이 편리할 것입니다. 저는 처음에 모든 Rule을 다 선택한 다음에warning들을 없애가면서 Rule들을 하나하나 검토해 나갔었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/pmd/eclipse-pmd-project.jpg&quot; alt=&quot;eclipse-pmd-project.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 화면에서 Enable PMD를 선택하면 .project파일에 아래와 같은 부분이 추가될 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;natures&amp;gt;
      ....
   &amp;lt;nature&amp;gt;net.sourceforge.pmd.eclipse.plugin.pmdNature&amp;lt;/nature&amp;gt;
&amp;lt;/natures&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이제 해당 프로젝트는 Eclipse의 Problems View에서 PMD에서 보고하는 error, warning까지 모두 다 뜨게됩니다. 개발을 하면서 Rule 준수에 대한 빠른 피드백을 얻기 위해 되도록 이 기능을 사용하는 것이 좋습니다. 대신 이미Project에 warning이 많으면 새로운 warning들이 잘 눈에 띄지 않게 되므로, Project를 warning없이깨끗이 정리한 다음에 사용할 것을 권장해 드립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모든 Rule을 다 적용하면 아마 굉장히 많은 warning과 때로는 에러까지도 나올 것입니다. 그런 것들을 다 검토해서 포함시킬지를 결정해야 합니다. &lt;a href=&quot;http://pmd.sourceforge.net/rule-guidelines.html를&quot; class=&quot;bare&quot;&gt;http://pmd.sourceforge.net/rule-guidelines.html를&lt;/a&gt; 참조하셔서, 프로젝트 상황에 맞게 적용해야겠죠. 그중 몇가지 Rule에 대해서만 언급을 하고 넘어가겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pmd.sourceforge.net/rules/basic.html&quot;&gt;Basic Rules&lt;/a&gt;-EmptryInitializer:  PMD 5.0에서 추가된 룰로 Maven의 PMD plugin버전 2.4에서는 PMD 버전 4.2.2를 참조하기 때문에 이Rule은 지원되지 않습니다. 따라서 PMD의 Eclipse plugin에서 이를 지원한다고 할지라도 Maven plugin과같이 쓰기 위해서는 이 Rule을 반드시 제외해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pmd.sourceforge.net/rules/optimizations.html&quot;&gt;Optimization Rules&lt;/a&gt;-LocalVariableCouldBeFinal과 Controversial Rules-AvoidFinalLocalVariable : 서로 상반되는 Rule로 한쪽 Rule을피하면 다른 쪽에 걸려드는 Rule입니다. 그래서 warning을 안보려면 둘 중에 하나는 꼭 제외해야 합니다. 그런데,final을 Local variable에 일일히 선언하는 것도 번거로운 일이고, 메서드 내의 inner class에서 참조해야되어서 꼭 final이 되어야하는 local variable도 있으므로, 둘 다 제외하는 것도 좋습니다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pmd.sourceforge.net/rules/controversial.html&quot;&gt;Controversial Rules&lt;/a&gt;-OnlyOneReturnRule :  메서드에서 return문이 여러 개일 경우 경고를 주는데, 메서드 중간의 return문은 복잡한 조건문의 구조를 단순하게 하는데 도움이 경우가 많고, &lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2824034&amp;amp;CategoryNumber=001001003016001006&quot;&gt;켄트벡의 구현패턴&lt;/a&gt; 7장 중 &apos;보호절&amp;#8217;을 보면 이를 권장하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pmd.sourceforge.net/rules/design.html&quot;&gt;Design Rules&lt;/a&gt;-UnnecessaryLocalBeforeReturn: return 전에 따로 local 변수로 반환할 값을 선언할 때 주는 경고인데, 기능적으로는 별 의미 없는 코드이나,return 문장에는 @SupressWarning 의 Annotation을 추가할 수 없기 때문에,  Annotation 적용범위를 최소화하기 위해 그런 선언이 필요한 때도 있습니다. (Java Language Spec 9.7, EffectiveJava 2nd Edition Item 24 참조)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;검토해보니 가장 부담없이 적용가능한 RuleSet이 괄호에 대한 규칙을 정의하는 Braces Rules이고, Controversial Rules가 이름 그대로 가장 제외할 것이 많은 Rule Set입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 과정을 거쳐서 선별된 Rule 정의 파일이 만들어지면 그것을 Maven의 PMD plugin에서도 참조할 수 있게 설정합니다. 저는 Rule설정 파일이름을 .ruleset으로 하고 pom.xml에 추가했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;  &amp;lt;reporting&amp;gt;
    &amp;lt;plugins&amp;gt;
      &amp;lt;plugin&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;maven-pmd-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;2.4&amp;lt;/version&amp;gt;
        &amp;lt;configuration&amp;gt;
          &amp;lt;rulesets&amp;gt;
            &amp;lt;ruleset&amp;gt;$\{basedir}/.ruleset&amp;lt;/ruleset&amp;gt;
          &amp;lt;/rulesets&amp;gt;
          &amp;lt;sourceEncoding&amp;gt;utf-8&amp;lt;/sourceEncoding&amp;gt;
          &amp;lt;targetJdk&amp;gt;1.6&amp;lt;/targetJdk&amp;gt;
          &amp;lt;minimumTokens&amp;gt;10&amp;lt;/minimumTokens&amp;gt;
        &amp;lt;/configuration&amp;gt;
      &amp;lt;/plugin&amp;gt;
  &amp;lt;/reporting&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고는 mvn site 혹은 mvn pmd:pmd pmd:cpd 처럼 PMD plugin의 goal을 포함시킨 빌드를 한번 실행시켜 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;문제가 없이 돌아갔으면 hudson에도 PMD플러그인을 설정합니다.. PMD 플러그인의 goal이 포함된 빌드를 돌리고 나면PMD warning라는 링크가 해당 프로젝트에 생기고, 거기서 아래와 같은 보고서를 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/pmd/hudson-pmd-report.jpg&quot; alt=&quot;hudson-pmd-report.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;관련자료&quot;&gt;관련자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ibm.com/developerworks/kr/library/j-pmd/&quot;&gt;PMD로 버그 잡기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pmd.sourceforge.net/rule-guidelines.html&quot;&gt;PMD Rule guideline&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://moai.tistory.com/541&quot;&gt;PMD로 코드리뷰 자동으로 수행하기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.empas.com/seeiris/14381547&quot;&gt;PMD 이클립스 플러그인 사용법&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://ecogeo.tistory.com/70&quot;&gt;Hudson : 리포트/차트 보기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Effective &amp;amp; Agile Java Generics</title>
      <link>https://blog.benelog.net//2173103.html</link>
      <pubDate>Tue, 16 Dec 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2173103.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;div class=&quot;title&quot;&gt;수정이력&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2019.04.13&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;유효하지 않은 JavaDoc 문서 링크 최신화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예제에서 Diamond 문법 활용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JUnit4에 대한 일반적인 설명 링크 제거&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`ServletRequest.getParameterMap()`에 대한 최신 JavaDoc 내용 추가&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;`List&amp;lt;ScheduledFuture&amp;lt;?&amp;gt;&amp;gt;`가 쓰인 예시를 Spring Integration의 소스에서 LogBack의 것으로 변경&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;generics가_들어간_테스트_코드를_통과시켜_봅시다&quot;&gt;Generics가 들어간 테스트 코드를 통과시켜 봅시다.&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래에 있는 테스트 1~5까지의  테스트 코드들을 모두 한번에 통과시키는 ListUtils.max메서드는 어떻게 선언하고 구현해야 할까요?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Generics를 써보신 분이라면 &lt;a href=&quot;http://benelog.springnote.com/pages/2231980/attachments/1026628?dummy=1229420647470&quot;&gt;ListUtilsTest.java&lt;/a&gt;를 다운 받으셔서 한번 풀어 보시기 바랍니다. Collections.max()를 아시는 분도 그 메서드를 참고하시지 마시고 직접 메서드를 만들어 보시면 재미있으실 겁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제약조건은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;@SuppressWarnings(&quot;unchecked&quot;)&lt;/code&gt; 를 쓰지 않고도 Generics에 대한 warning이 없고, Casting도 한번도 하지 않아야 하고&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;컴파일 시점에서 ListUtils.max 메서드에 Comparable 인터페이스를 구현한 객체들을 쌓은 List가 넘어온다는 것을 검증할 수 있어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;테스트 1 :  빈 리스트가 넘어오면 null값 반환&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Test
    public void getNullIfEmptyList(){
        List&amp;lt;Integer&amp;gt; numbers = new ArrayList&amp;lt;&amp;gt;();
        Integer max = ListUtils.max(numbers);
        assertThat(max,is(nullValue()));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;테스트 2 :  Integer객체의 최대값 구하기&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Test
    public void getMaxInteger(){
        List&amp;lt;Integer&amp;gt; numbers = new ArrayList&amp;lt;&amp;gt;();
        numbers.add(Integer.valueOf(1))
        numbers.add(Integer.valueOf(2));
        Integer max = ListUtils.max(numbers);
        assertThat(max,is(Integer.valueOf(2)));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;테스트 3 :  BigInteger객체의 최대값 구하기&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Test
    public void getMaxBigInteger(){
        List&amp;lt;BigInteger&amp;gt; numbers = new ArrayList&amp;lt;&amp;gt;();
        numbers.add(BigInteger.ZERO);
        numbers.add(BigInteger.ONE);
	BigInteger max = ListUtils.max(numbers);
        assertThat(max,is(BigInteger.ONE));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;테스트 4: java.sql.Date 객체의 최대값 구하기&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Test
    public void getMaxDate(){
        java.sql.Date now = new java.sql.Date(new Date().getTime());
        java.sql.Date afterAWhile = new java.sql.Date(new Date().getTime()+6000);
        List&amp;lt;java.sql.Date&amp;gt; dates = new ArrayList&amp;lt;java.sql.Date&amp;gt;();
        dates.add(now);
        dates.add(afterAWhile);
        java.sql.Date max = ListUtils.max(dates);
        assertThat(max,is(afterAWhile));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;테스트5 : ScheduledFuture를 구현한 객체의 최대값 구하기&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    @Test
    public void getMaxScheduledFuture() throws InterruptedException{
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        ScheduledFuture&amp;lt;?&amp;gt; after1Second = executor.schedule(getTask(&quot;first&quot;),1,TimeUnit.SECONDS);
        ScheduledFuture&amp;lt;?&amp;gt; after2Seconds = executor.schedule(getTask(&quot;second&quot;),2,TimeUnit.SECONDS);
        List&amp;lt;ScheduledFuture&amp;lt;?&amp;gt;&amp;gt; futures = new ArrayList&amp;lt;ScheduledFuture&amp;lt;?&amp;gt;&amp;gt;();
        futures.add(after1Second);
        futures.add(after2Seconds);
        ScheduledFuture&amp;lt;?&amp;gt; max = ListUtils.max(futures);
        long maxDelay = max.getDelay(TimeUnit.SECONDS);
        assertThat(maxDelay,is(after2Seconds.getDelay(TimeUnit.SECONDS)));
        Thread.sleep(3000);
        assertTrue(max.isDone());
    }
    private Runnable getTask(final String message) {
        Runnable task = new Runnable(){
            public void run() {
                System.out.println(message);
            }
        };
        return task;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/index.html?java/util/concurrent/ScheduledFuture.html&quot; class=&quot;bare&quot;&gt;https://docs.oracle.com/javase/8/docs/api/index.html?java/util/concurrent/ScheduledFuture.html&lt;/a&gt;]  API문서 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;풀이와_설명&quot;&gt;풀이와 설명&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트 1,2,3번 까지의 코드만이라면 아래와 같이 선언하셔도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트 1: 간단한 max 메서드 선언&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; T max(List&amp;lt;T&amp;gt; list)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 &lt;code&gt;&amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; 처럼 Type parameter가 그 자신이 포함된 표현으로 그 범위가 선언되는 것을 recursive type bound라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Integer와 BigDecimal의 클래스 선언을 보면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트 2: Integer 와 BigInteger 클래스 선언부&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public final class Integer extends Number implements Comparable&amp;lt;Integer&amp;gt;

public class BigInteger extends Number implements Comparable&amp;lt;BigInteger&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 클래스 모두 자신의 타입이 Parameterized type으로 들어간 Comparable 인터페이스를 구현하고 있기 때문에 리스트3의 메서드 선언으로도 Integer나 BigInteger가 담긴 리스트를 받을 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 리스트 1의 선언으로는 테스트4,5에 있는 메서드에서 컴파일 에러가 날 것입니다. 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;테스트4의 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/sql/Date.html&quot;&gt;java.sql.Date&lt;/a&gt;는 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/Date.html&quot;&gt;java.util.Date&lt;/a&gt;를 상속한 클래스입니다.
그런데 &lt;code&gt;java.sql.Date&lt;/code&gt; 는 따로 comparesTo 메서드를 재정의하고 있지 않고, 상위클래스인 &lt;code&gt;java.util.Date&lt;/code&gt; 에 있는 메서드를 그대로 쓰고 있습니다.
&lt;code&gt;java.sql.Date&lt;/code&gt; 는 &lt;code&gt;Comparable&amp;lt;java.sql.Date&amp;gt;&lt;/code&gt; 한 것이 아닌 &lt;code&gt;Comparable&amp;lt;java.util.Date&amp;gt;&lt;/code&gt; 를 구현한 것이라고 볼 수 있습니다.
(두 클래스의 이름이 같아서 혼동이 되실 수도 있습니다.
&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2600965&amp;amp;CategoryNumber=001001003016003014&quot;&gt;Java Puzzler&lt;/a&gt;에서는 이 두 클래스의 예를 들면서 자바 플랫폼 설계자가 이름을 지으면서 깜빡 존 듯하다고 언급하고 있습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 테스트5의 &lt;code&gt;java.util.concurrent.ScheduledFuture&lt;/code&gt; 인터페이스는 &lt;code&gt;Comparable&amp;lt;ScheduledFuture&amp;gt;&lt;/code&gt; 를 구현한 것이 아닌, &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Delayed.html&quot;&gt;Delayed&lt;/a&gt;라는 인터페이스를 상속한 것이고, 이 Delayed는 &lt;code&gt;Comparable&amp;lt;Delayed&amp;gt;&lt;/code&gt; 를 상속한 인터페이스입니다. 리스트 3의 인터페이스 선언을 보시면 쉽게 이해가 되실 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트 3: Delayed 인터페이스 선언부&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public interface ScheduledFuture&amp;lt;V&amp;gt; extends Delayed, Future&amp;lt;V&amp;gt;

public interface Delayed extends Comparable&amp;lt;Delayed&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 경우도 모두 통과할 수 있게 &lt;code&gt;ListUtils.max()&lt;/code&gt; 메서드를 선언하고 구현하면 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트4: ListUtils 구현&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class ListUtils {
    public static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(List&amp;lt;T&amp;gt; list){
        T result = null;
        for(T each : list) {
            if (result==null) result = each;
            if(each.compareTo(result)&amp;gt;0) result = each;
        }
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;public static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(List&amp;lt;T&amp;gt; list)&lt;/code&gt; 라는 긴 메서드 선언입니다. 이 선언 안에는 recursive type bound, wild card, upper bound, lower bound가  다 들어가 있습니다. 이 정도 메서드를 설계할 수 있어야지, Java generics를 제대로 아는 것이라고 할 수 있겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;bounded wild card를 적용하는 기준은 Effective Java 2nd Edition에 나와 있는 PECS(Producer-extends, Consumer-super)원칙을 기억하시면 도움이 됩니다. 원래 &lt;a href=&quot;http://engdic.daum.net/dicen/contents.do?query1=ED00866730&quot;&gt;PECS의 뜻&lt;/a&gt;은 가슴 근육이라는군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;t_extends_comparable&quot;&gt;&lt;code&gt;&amp;lt;T extends Comparable&amp;#8230;&amp;#8203;.&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Comparable인터페이스를 구현한 클래스가 그 대상이어야 max내부에서 Comparable.compareTo를 이용해서 최대값을 구할 수 있습니다. 그래서 타입 T는 T extends Comparable이 되어야 합니다. PECS원칙으로도 리턴값으로 생산되는 (Producer) 타입이 T이므로 extends를 쉽게 연상하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;comparable_super_t_부분&quot;&gt;&lt;code&gt;Comparable&amp;lt;? super T&amp;gt; 부분&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;max 메서드 내부에서 타입 T는 Comparable.compareTo(T o)메서드 뒤에 파라미터로 넘어가는, 소비되는(Consumer) 대상으로 쓰이기에 PECS원칙으로 super로 연결시킬 수 있습니다. 테스트5의 코드를 예로 보면, ScheduledFuture는 ScheduledFuture의 상위 인터페이스인 Delayed가 Comparable의 Parameterized type으로 넘어가는 Comparable&amp;lt;Delayed&amp;gt;형태의 Comparable인터페이스를 상속하고 있습니다.  T를 ScheduledFuture로 봤을 때 Comparable&amp;lt;? super T&amp;gt;는 Comparable&amp;lt;Delayed&amp;gt;와 잘 맞아떨어집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 리스트4의 ListUtils.max 메서드는 &lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2416238&amp;amp;CategoryNumber=002&quot;&gt;Effective Java 2nd Edition&lt;/a&gt;의 Item28에 나오는 코드를 보고서 약간 변경을 해 본 것입니다. 원래 책에 나오는 코드는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트5: Effective Java 2nd Edition에 있는 max메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(List&amp;lt;? extends T&amp;gt; list){
  Iterator&amp;lt;? extends T&amp;gt; i = list.iterator();
  T result = i.next();
  while(i.hasNext()){
            T t = i.next();
            if (t.compareTo(result)&amp;gt;0) result = t;
   }
   return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;메서드 선언이 &lt;code&gt;public static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(List&amp;lt;? extends T&amp;gt; list)&lt;/code&gt; 로 예제보다 더 늘어난 부분이 있습니다. 끝에 있는 &lt;code&gt;List&amp;lt;? extends T&amp;gt;&lt;/code&gt; 가 추가된 것입니다. 이 부분은 PECS원칙에 따르면 List객체로부터 T를 생산(Producer)해 오기 때문에 &lt;code&gt;? extends T&lt;/code&gt; 로 하는 것이 적절해 보이는 합니다. 그러나 테스트1~5의 코드에서는 &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; 만으로도 컴파일러가 수행하는 형추론(type inference)에 문제가 없었기에 제가 만든 코드인 리스트4에는 추가하지는 않았습니다. 컴파일러가 수행하는 Type inference는 굉장히 복잡하고, Java Language Spec에서 16페이지나 차지한다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 리스트5에서는 길이가 0인 List가 넘어간 값일 때는 첫번째 i.next();에서 NoSuchElementException을 내게 되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#max-java.util.Collection-&quot;&gt;java.util.Collections.max&lt;/a&gt; 메서드에서도 같은 결과가 나오는 것으로 보아서, 유사한 구현방식이 쓰인 것으로 추측됩니다. 제가 만든 문제에서는 Collection.max와 약간 다른 부분을 만들어 보고 싶어서 길이가 0일 list가 올 때는 null을 반환하는 방식으로 바꾸어 보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다면 `java.util.Collections.max`의 메서드 시그니처는 어떻게 되어 있을까요?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트6: java.util.Collections.max 메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static &amp;lt;T extends Object &amp;amp; Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(Collection&amp;lt;? extends T&amp;gt; coll)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일단 대상이 List보다 확장된 Collection이니 Parameter가 Collection인 것이 눈에 들어 옵니다. 그런데 T의 제약조건이 &amp;lt;T extends Object &amp;amp; Comparable&amp;lt;? super T&amp;gt;&amp;gt;로 선언되어 있는 것이 리스트5의 코드보다 &apos;Object &amp;amp;&apos; 부분이 더 들어가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이것은 java1.4와의 하위 호환성을 위한 것입니다. Java에서는 하위호환성 지원 때문에 컴파일 시에 Generics관련 정보를 모두 검사한 후에는 실제로는 Generics 정보가 전혀 없는 바이트코드를 생성하게 되어 있습니다. 그래서 리스트5처럼 메서드를 선언했을 때에는 런타임시에는 리스트 7과 같은 코드와 같은 바이트코드가 생성됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트 7: 리스트5의 메서드 선언이 자료형 지우기가 수행된 뒤의 모습&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static Comparable max(Collection c)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 이전 버전에서의 max메서드의 모습은 다음과 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트8: Java5 이전의 Collections.max 메서드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public static Object max(Collection c)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 리스트7처럼 Comparable을 반환하게 된다면 이것은 이전버전의 메서드 Signature를 바꿔버린 것이 되므로 하위버전에서 컴파일된 코드에서 Collections.max를 호출할 때 에러를 발생시키게 됩니다. 그래서 Object &amp;amp;이 더 추가된 것이죠. (Agile Java의 Lesson 12 중 Additional Bounds에서 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기까지 이해하셨으면, 실무에서 어떤 Generics 관련 코드를 봐도 이제 쉬워보이실 겁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;generics의_표현력&quot;&gt;Generics의 표현력&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;하나의 예제로 Generics의 많은 부분을 설명하기 위해서 다소 복잡한 코드를 보여드렸습니다. 혹시나 Generics를 이제 막 적용하시고 싶으신 분들의 마음을 어둡게 한 것이 아닌가 걱정이 되기도 합니다. 그러나 대부분의 Generics적용 사례는 훨씬 간단하고, 특히 Collection 선언에 genercics를 활용하는 정도는 어렵지 않습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Generics는 컴파일시점에서의 에러 검출 영역을 넓혀줘서 보다 이른 시점에 버그를 잡을 수 있게 해주고, 코드I의 설명력을 높여줘서 API사용자들이 보다 쉽게 API를 쓸 수 있게 해줍니다. 컴파일타입의 에러체크 능력은 위의 예제를 통해서 설명했으니, 표현력에 대해서도 제가 겪은 사례를 이야기해 볼까 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오래　전에 저는 Java 인터페이스를 엑셀파일로 만드는 산출물 작업을 했었는데, 리턴타입이 List인 메서드들은 그 안에 어떤 객체들이 들어있는지 메서드 시그니처만으로는 표현할 수 없어서 답답했던 적이 있었습니다. 그래서 아예 List대신 배열을 쓸까도 고민하다가 List가 가진 편의성들을 버릴 수가 없어서 List를 쓰고 따로 문서에 그 안에 어떤 객체가 들어가 있는지를 적을 수 밖에 없었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javaee/5/api/javax/servlet/ServletRequest.html#getParameterMap()&quot;&gt;javax.servlet.ServletRequest.getParameterMap()&lt;/a&gt;를 사용할 때는 API사용자로서 아쉬움을 느꼈었습니다. API문서를 보면 이 메서드가 반환하는 Map에는 key로 String이, value로 String배열이 들어가 있는 것으로 설명되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/java/request-getParametersMap.gif&quot; alt=&quot;request getParametersMap&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 처음에 이 문서를 안 보고 key가 String, 값이 하나일 때는 그냥 String, 값이 2개 이상이면 String배열이 들어가 있지 않을까하는 추측을 바탕으로 한 코드를 짜서 몇번 에러를 냈었었습니다.
결국 API문서를 보고서 어떤 형식으로 자료가 들어가 있는지 알게 되어있습니다.
이 메서드의 리턴타입이 &lt;code&gt;Map&amp;lt;String,String[]&amp;gt;&lt;/code&gt; 과 같이 선언되어 있었다면, 문서를 안 보고도, Runtime 에러를 안 겪고도 바로 올바른 자료형으로 사용이 가능했을 것입니다.
이렇듯 Generics를 잘 활용할 수 없는 API를 쓸 때에는 소비자 입장에서 불편합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;나중에 저는 이 메서드를 호출하는 부분을 아래와 같이 감싸는 부분을 넣었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;리스트9: &lt;code&gt;javax.servlet.ServletRequest.getParameterMap()&lt;/code&gt; 메서드를 Generics를 이용한 코드로 감싸기&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;@SuppressWarnings(&quot;unchecked&quot;)
Map&amp;lt;String,String[]&amp;gt; requestMap = request.getParameterMap();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;@SuppressWarnings(&quot;unchecked&quot;)&lt;/code&gt; 은 어쩔 수 없는 경우에만 써야 하고, 쓸 때도 클래스 단위, 메서드 단위가 아닌 이런 최소 라인 단위로 써야 합니다.
Effective Java 2nd Edition Item 24참조)
이 경우는 Generics지원하지 않는 외부 인터페이스를 호출하는 것이라서 불가피한 경우입니다.
형에 대해서는 API 문서에 명시된 내용라서 이렇게 @SuppressWarnings을 선언해도 문제가 없습니다.
필요에 따라서 이 requestMap을 리턴해 준다면 그것을 쓰는 코드에서는 더 이상 이 안에 무엇이 들어있는지 문서를 찾아보지 않아도 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#getParameterMap()&quot;&gt;
JavaEE6 버전의 JavaDoc&lt;/a&gt;을 보니 이제는 &lt;code&gt;request.getParamersMap`의 반환형이 `Map&amp;lt;String,String[]&amp;gt;&lt;/code&gt; 으로 바뀌어 있음을 확인할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/java/request-getParametersMap-ee6.png&quot; alt=&quot;request getParametersMap ee6&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;관련자료_모음&quot;&gt;관련자료 모음&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;generics_관련_자료&quot;&gt;Generics 관련 자료&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 포스트는 주로 아래 두 책을 보면서 얻은 정보를 통해 작성되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2416238&amp;amp;CategoryNumber=002&quot;&gt;Effective Java 2nd Edition&lt;/a&gt; : &lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2416238&amp;amp;CategoryNumber=002&quot; class=&quot;bare&quot;&gt;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2416238&amp;amp;CategoryNumber=002&lt;/a&gt;Chater 4: Item23 ~ Item 29&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=1534922&amp;amp;CategoryNumber=002001026004&quot;&gt;Agile Java&lt;/a&gt; : &lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=1534922&amp;amp;CategoryNumber=002001026004&quot; class=&quot;bare&quot;&gt;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=1534922&amp;amp;CategoryNumber=002001026004&lt;/a&gt;Lesson 14&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Agile java처럼 테스트 코드를 먼저 보여주었고 , ListUtils.max 메서드는 Effective Java의 내용을 주로 참조해서 작성했습니다. Effective Java에서는 ScheduledFuture의 경우에 대해서 언급만 되어 있고 예제코드가 없는 것이 아쉬워서 테스트5의 코드를 작성했고, 비슷한 사례의 보다 친숙한 클래스를 찾다가  Agile java에서 java.sql.Date 클래스가 예제에 많이 쓰인 것을 보고 테스트4를 추가했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2416238&amp;amp;CategoryNumber=002&quot;&gt;Effective Java 2nd Edition&lt;/a&gt; 에 포함된 내용 중 Java5 관련 내용은 Joshua Bloch이 했던 발표에 잘 요약되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://developers.sun.com/learning/javaoneonline/2006/coreplatform/TS-1512.pdf&quot;&gt;Effective Java Reloaded, JavaOne Conference 2006&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://javapolis.libsyn.com/index.php?post_id=380974&quot;&gt;Effective Java Reloaded - 2008&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그외 Generics에 관한 자료들의 링크는 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf&quot;&gt;Genercis tutorial&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://mkseo.pe.kr/blog/?p=1691&quot;&gt;Summary on some java generics presentations/postings&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ibm.com/developerworks/kr/library/j-jtp04298.html&quot;&gt;자바 이론과 실습: 제네릭스 해부, Part 1&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.me/1932&quot;&gt;Generic 팩토리 메서드&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ibm.com/developerworks/kr/library/j-jtp07018.html&quot;&gt;자바 이론과 실습: 제네릭스 해부, Part 2&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://gafter.blogspot.com/2006/12/super-type-tokens.html&quot;&gt;Super Type Tokens&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ibm.com/developerworks/java/library/j-jtp01255.html&quot;&gt;Generics gotchas&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.tistory.com/1614&quot;&gt;Generic 메타데이터 활용하기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;concurrent_관련자료&quot;&gt;Concurrent 관련자료&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Effective Java에서 언급한 ScheduledFuture를 이용한 예제코드를 만들다 보니 Concurrent관련 API들이 몇개 포함되었습니다. 그 API들에 관심이 있으신 분은 아래 자료를 참조하시면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=3015162&amp;amp;CategoryNumber=001001003016003014&quot;&gt;자바 병렬프로그래밍 :&lt;/a&gt; 6장 작업실행&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;관련 API 문서&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/Executors.html&quot;&gt;java.util.concurrent.Executors&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ScheduledExecutorService.html&quot;&gt;java.util.concurrent.ScheduledExecutorService&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ScheduledThreadPoolExecutor.html&quot;&gt;java.util.concurrent.ScheduledThreadPoolExecutor&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;List&amp;lt;ScheduledFuture&amp;lt;?&amp;gt;&amp;gt;&lt;/code&gt; 의 코드가 실전에서 쓰인 것이 없을까해서 찾아보니 로깅　프레임워크인　Logback의　소스 코드에서 그런 코드가 발견되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/qos-ch/logback/blob/master/logback-core/src/main/java/ch/qos/logback/core/ContextBase.java#L52［ContextBase.java］&quot; class=&quot;bare&quot;&gt;https://github.com/qos-ch/logback/blob/master/logback-core/src/main/java/ch/qos/logback/core/ContextBase.java#L52［ContextBase.java］&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>스프링 프레임웍의 최근 소식들 모음 등</title>
      <link>https://blog.benelog.net//2166348.html</link>
      <pubDate>Wed, 10 Dec 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2166348.html</guid>
      	<description>
	&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://javajigi.tistory.com/207&quot;&gt;Spring Source 진영의 향후 RoadMap&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ksug.org/77&quot;&gt;SpringSource의 또 다른 야심작, tc Server&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ksug.org/72&quot;&gt;&quot;Spring Python&quot; 파이썬 개발자에게도 봄(?)을&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ksug.org/68&quot;&gt;Spring, Grails를 품에 안다&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ksug.org/79&quot;&gt;Spring One America 2008 참석기 링크 모음&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.springsource.com/2008/12/05/spring-framework-30-m1-released/&quot;&gt;First Spring Framework 3.0 milestone released&lt;/a&gt;http://whiteship.me/2091[]&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.me/2091&quot;&gt;스프링 프레임워크 3.0 M1 배포~&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;역시 최근 들어 스프링소스의 발걸음이 더욱 빨라졌습니다. Grails, Spring Python, tc Server, 3.0M1 발표.. 현란하게 소식이 쏟아지는군요. 그만큼 공부할 것도 더 늘겠지만, 그래도 반갑고 기대감을 가지게 하는 소식들입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대규모 투자를 받고, 일을 더 벌이고 하면서 스프링소스가 돈맛에 초심을 잃을까봐 걱정을 하시는 분도 많이 계시는 듯 합니다. 그래도 성공적인 사업 구조가 정착되어서 그 수익이 개발자들에게 도움이 되는 제품들을 더 잘 개발하는데 쓰인다면야 굳이 돈을 많이 벌게 되는 것이 나쁘다고는 생각하지 않습니다. 유료로 제공되는 제품이 많아진다고 해도, 그럴만한 가치가 있는 제품이면 돈을 내고, 그것이 아니라도 프레임웍이나 서버, 개발도구의 시장은 독점 시장이 아니니 다른 선택을 할 수 있는 대안이 많을 것입니다. 물론 지금까지의 스프링의 행보를 봐서는 충분히 앞으로도 가치있는 결과물을 만들 것이라고 기대하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여러 소식들 중에 솔직히 가장 재미있었던 것은, Spring one America 2008에 참석하신 분들을 통해 알게된, 로드존슨의 나이였습니다. 스프링 핵심 개발자인 유겐할러의 말에 의하면 로드존슨은 38살 정도라고 하네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;왜 저는 로드존슨을 당연히 40대라고 생각했을까.. 하고 분석해보니, Expert one-on-one J2EE Design and Developement 책 때문에 그런 선입관을 가지게 된 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책이 나온 것이 2003년인데, 그렇다면 이 때는 33살이였단 말이되는군요.
이 책을 쓰는데도 2년 정도가 걸렸다고 들은 것 같습니다.
그리고 책 표지에 보면 로드존슨은 음악학(Musicology)에 박사학위가 있다는 말도 있습니다. 그래서, 다른 분야도 박사까지 하고 컴퓨터 분야에서도 이정도 경지까지 가려면 적어도 이 책 출판시에 30대 중후반은 되지 않았을까.. 하는 추측을 한 것이죠.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>FindBugs + Eclipse + Maven2 + Hudson</title>
      <link>https://blog.benelog.net//2079841.html</link>
      <pubDate>Tue, 30 Sep 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2079841.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://findbugs.sourceforge.net/&quot;&gt;FindBugs&lt;/a&gt;를 이용한 코드검사를 Maven2을 통해 실행하고, Hudson을 통해 확인하는 설정을 정리해 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eclipse에서 findbugs로 코드검사를 해볼 수 있는 툴은 &lt;a href=&quot;http://findbugs.cs.umd.edu/eclipse/&quot; class=&quot;bare&quot;&gt;http://findbugs.cs.umd.edu/eclipse/&lt;/a&gt; 를 update site로 지정하면 설치할 수 있습니다. 설치가 잘 되었다면 소스 폴더를 선택하고 마우스 우클릭을 하면 &apos;Find Bugs&amp;#8217;라는 메뉴가 생긴 것이 보일 것입니다.
그 메뉴를 통해 원하는 프로젝트를 검사하고, Bug Explorer 탭을 선택해보면 아래와 같은 화면이 나옵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/eclipse-findbugs.jpg&quot; alt=&quot;eclipse-findbugs.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bug Explorer 탭에서는 버그를 유형별로 정리해서 보여주고, 소스탭에서는 해당하는 코드에 벌레 모양 아이콘을 찍어줍니다. 그리고 Problems 창에서는 Eclipse에서 잡아내는 다른 경고처럼 warning으로 해당 소스를 표시해 줍니다. Bug Details 탭을 누르면 버그에 대한 자세한 설명도 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;프로젝트의 Properties 메뉴에서 FindBugs 설정란으로 가면 검사할 규칙 등을 선택할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/eclipse-findbugs-config1.jpg&quot; alt=&quot;eclipse-findbugs-config1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 설정화면에서 &apos;Run FindBugs automatically&amp;#8217;를 선택하면, 소스가 바뀔 때마다 자동으로 검사를 수행해 줍니다. 이 기능이 선택되어 있지 않다면, 지적된 소스를 수정해도 다시 수동으로 검사를 돌려야지 경고메시지 지워지므로, 이클립스가 아주 느리다는 느낌이 안 들정도라면 선택하는 것이 좋습니다. 이 기능을 선택해서, Eclipse의 Problems 탭에서 코드 작성 즉시 에러와 경고를 확인할 수 있게 되었다면 .project 파일에 아래와 같은 부분이 추가되어 있을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;natures&amp;gt;

    ....

  &amp;lt;nature&amp;gt;edu.umd.cs.findbugs.plugin.eclipse.findbugsNature&amp;lt;/nature&amp;gt;

&amp;lt;/natures&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FindBugs 설정 화면 중 Detector Configuration 탭에서는 검사할 규칙들을 지정할 수 있고, Reporter configuration 탭에서는 보고해 줄 버그의 경고단계와 분류를 선택할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/eclipse-findbugs-config2.jpg&quot; alt=&quot;eclipse-findbugs-config2.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Filter files 탭에서는 별도의 XML파일로 선언된 포함하거나 제외시킬 버그와 파일에 대한 설정을 가지고 올 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/eclipse-findbugs-config3.jpg&quot; alt=&quot;eclipse-findbugs-config3.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래에 자세히 설명하겠지만, Maven 설정에서 참조하는 findBugsExclude.xml을 Eclipse plugin에서도 똑같이 지정해서 Maven과 Eclipse에서 같은 기준으로  검사가 수행되도록 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Maven2의 &lt;a href=&quot;http://mojo.codehaus.org/findbugs-maven-plugin/&quot;&gt;findbugs-maven-plugin&lt;/a&gt;은  pom.xml에 아래와 같이 설정됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;plugin&amp;gt;
  &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;findbugs-maven-plugin&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;2.4.0&amp;lt;/version&amp;gt;
  &amp;lt;configuration&amp;gt;
    &amp;lt;findbugsXmlOutput&amp;gt;true&amp;lt;/findbugsXmlOutput&amp;gt;
    &amp;lt;findbugsXmlWithMessages&amp;gt;true&amp;lt;/findbugsXmlWithMessages&amp;gt;
    &amp;lt;xmlOutput&amp;gt;true&amp;lt;/xmlOutput&amp;gt;
    &amp;lt;excludeFilterFile&amp;gt;$\{basedir}/findBugsExclude.xml&amp;lt;/excludeFilterFile&amp;gt;
   &amp;lt;/configuration&amp;gt;
&amp;lt;/plugin&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 설정에 들어가는 속성들에 대해서는 &lt;a href=&quot;http://mojo.codehaus.org/findbugs-maven-plugin/findbugs-mojo.html&quot;&gt;findbugs-maven-plugin 설명 페이지&lt;/a&gt;에서 자세한 내용을 보실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 제외할 검사규칙을 지정하기 위해서 excludeFilterFile속성에 findBugsExclude.xml을 지정했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;findBugsExclude.xml의 내용은 아래와 같이 설정했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;xml&quot;&gt;....
&amp;lt;FindBugsFilter&amp;gt;
....
....
  &amp;lt;Match&amp;gt;
....
....
   &amp;lt;Bug code=&quot;Se,SnVI,Dm,UwF,EI,EI2&quot; /&amp;gt;
....
....
  &amp;lt;/Match&amp;gt;
....
....
&amp;lt;/FindBugsFilter&amp;gt;
....&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제외할 것을 선언하는 파일에 이렇게 적었으니 Bug code가 &quot;Se,SnVI,Dm&quot;에 해당하는 버그검사는 제외한다는 의미입니다. Filter의 설정 방법에 대해서는 &lt;a href=&quot;http://mojo.codehaus.org/findbugs-maven-plugin/findbugs-mojo.html&quot;&gt;findbugs의 매뉴얼&lt;/a&gt;을 참조하시면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;버그 코드 중 Se,SnVI는 serialVersionUID에 관한 것이고 Dm은 String.toUpperCase 등의 메서드에서 Local설정을 권유하는 검사입니다.   (&lt;a href=&quot;http://findbugs.sourceforge.net/bugDescriptions.html&quot;&gt;버그 코드에 대한 설명 페이지&lt;/a&gt; 참조)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이렇게 설정을 하고 &lt;code&gt;mvn findbugs:findbugs&lt;/code&gt; 로 maven을 실행시키면 필요한 라이브러리들을 다운로드 받고 빌드가 실행됩니다. 실행이 성공했다면 목적지 폴더에 findbugs.xml과 findbugsXml.xml파일이 생성이 되었을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이것을 Hudson을 통해서 보기 위해서는 Hudson에서도 findbugs plugin을 설치해야 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Hudson 첫 화면에서 Manage Hudson - Manage Plugins 메뉴를 찾아갑니다. Available 탭에서 findbugs를 선택하고 화면 우측하단의 &apos;install&amp;#8217;버튼을 누르면 Hudson이 알아서 라이브러리를 다운 받아줍니다. 설치한 plug-in이 실행되기 위해서는 Hudson을 재시작해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 다음에 findbugs를 적용하고자 하는 프로젝트에 가서 Configure메뉴를 선택하면 아래와 같이 Publish FindBugs Analysis Result라는 부분이 추가된 것을 보실 수 있을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/hudson-findbugs-config.jpg&quot; alt=&quot;hudson-findbugs-config.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이것을 선택하고 원하는 기준값이 있을 경우 입력한 뒤에 &quot;save&amp;#8217;버튼을 누르고 build를 해보면 됩니다. 물론 build에는 findbugs:findbugs goal이 포함되어야 하겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;빌드가 성공하는 것을 보고 프로젝트의 메뉴를 보면 FindBugs Warnings라는 메뉴가 추가된 것을 확인하실 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/hudson-findbugs-menu.jpg&quot; alt=&quot;hudson-findbugs-menu.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 메뉴를 누르면 생성된 보고서가 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/findbugs/hudson-findbugs-report.jpg&quot; alt=&quot;hudson-findbugs-report.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;warning이 존재할 경우 건수를 클릭하면 해당하는 클래스들이 나오고, 클래스를 선택하면 소스에서 warning을 발생시키는 부분까지 보여줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;만약 hudson의 findbugs plugin을 실행할 때 Cannot find setter nor field in org.apache.maven.plugins.site.SiteMojo for &apos;xmlOutput&apos; 와 같은 에러가 난다면 &lt;a href=&quot;http://blog.benelog.net/2208375&quot;&gt;Hudson plugin 수동으로 빌드&amp;amp;업로드&lt;/a&gt;를 참조해서 최신 버전으로 플러그인을 업데이트 해보시기 바랍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;관련_자료&quot;&gt;관련 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://okjsp.tistory.com/1165643579&quot;&gt;Sun Techdays 2008 Lightning Talk 발표자료; findbugs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://okjsp.tistory.com/1165643626&quot;&gt;Hudson의 Findbugs 플러그인 이용하기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://okjsp.tistory.com/1165643570&quot;&gt;findbugs eclipse plugin 설치&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.buggymind.com/177&quot;&gt;FindBugs: 코드의 정적 분석을 통한 버그 탐색&lt;/a&gt; : 동영상 강연을 보니 구글에서도 이 도구를 사용하고 있다고 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Cubrid에서 commit시에 Statement가 닫히는 현상</title>
      <link>https://blog.benelog.net//2073073.html</link>
      <pubDate>Thu, 25 Sep 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2073073.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음의  코드는 1000건을 insert하면서 100건마다 한번씩 commit을 하는 기능을 수행합니다. 대용량 데이터를 입력 할 때 이렇게 주기적으로 commit을 해주는 것은 일반적인 처리방식입니다. Cubrid DB에서는 이와 같은 코드를 실행시키면 &apos;Attempt to access a closed PreparedStatement.&apos;라는 에러 메시지를 받게 될 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.Properties;

public class JDBCTest {

  public static void main(String[] args) {
    String query = &quot;INSERT into common_cd(cd_type, cd, cd_name) values(?, ?, ?);&quot;;
    Properties cubrid = new Properties();
    cubrid.put(&quot;url&quot;,&quot;jdbc:cubrid:127.0.0.1:43003:test:::&quot;);
    cubrid.put(&quot;driver&quot;,&quot;cubrid.jdbc.driver.CUBRIDDriver&quot;);
    cubrid.put(&quot;user&quot;,&quot;test&quot;);
    cubrid.put(&quot;password&quot;,&quot;test&quot;);

    insertAndCommit(cubrid, query);
 }

  private static void insertAndCommit(Properties prop,String query) {
    Connection con =  null;
    PreparedStatement psmt  = null;
    try{
     Class.forName (prop.getProperty(&quot;driver&quot;));
     con = DriverManager.getConnection(prop.getProperty(&quot;url&quot;),prop);
     con.setAutoCommit(false);
     psmt = con.prepareStatement(query);
     for(int i=1;i&amp;lt;=1000;i++)\{
       psmt.setString(1, &quot;00&quot;);
       psmt.setString(2, String.valueOf(i));
       psmt.setString(3, &quot;code:&quot;+i);
       psmt.execute();
       System.out.println(i+&quot;th row set&quot;);
       if(i%100==0) {
         con.commit();
         System.out.println(&quot;commit-------------------------------------&quot;);
       }
     }
    } catch (Exception ex){
      ex.printStackTrace();
    } finally{
     try{ psmt.close();} catch(Exception e){}
     try{ con.close();} catch(Exception e){}
    }
 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;commit 을 했을 뿐인데, Statement가 닫혀버리는 현상이 발생한 것이죠. 원인을 찾기 위해 Cubrid JDBC드라이버의 클래스 파일들을 역컴파일 해서 봤더니, 아래와 같은 completeAllStatements()라는 메서드가 commit 메서드 안에서 호출이 되는 것이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;private void completeAllStatements() throws SQLException {
    for (int i = 0; i &amp;lt; statements.size(); i++) {
        UniSQLStatement unisqlstatement = (UniSQLStatement) statements.get(i);
        if (unisqlstatement instanceof UniSQLPreparedStatement) {
            statements.remove(i);
            if (u_con.brokerInfoStatementPooling())   unisqlstatement.complete();
            else   unisqlstatement.close();
        } else{
        unisqlstatement.complete();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Connection의 close시에 Statement가 close되는 것은  JDBC명세에 나와있지만 commit시에 close될 수 있다는 것은 의외였습니다. 혹시나 싶어서 MySql, Oracle, Hsql의 JDBC구현들을 다 살펴보았지만, 비슷한 동작을 할 가능성이 있는 코드들은 찾을 수가 없었습니다. Cubrid만의 독특한 점이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 문제를 해결하는 방법은 DB 서버에 있는 설정파일인 Cubrid_broker.conf 파일에서 STATEMENT_POOLING속성을 ON으로 표시해주는 것이라고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 속성은 서버에서 Statement Pooling을 하겠다는 의미가 아니고, Connection을 받아서 쓰는 Client 쪽에서 Connection Pooling을 하고 있으니 Commit시에 Statement를 끊지 말라고 알려주는 것이라고 합니다. 위에 있는 completeAllStatements메서드 안의  u_con.brokerInfoStatementPooling()코드가 그것을 검사하는 것 같습니다. 사실 앞의 코드의 commit을 여러번 하는 예는 Statement pooling도 아니고 한 번 만들어진 Statement를 계속 쓰는 것 뿐인데, 이것을 Statement pooling을 쓰는 것으로 인식시켜야 한다니 논리적으로 이상하기는 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고, Cubrid에 접근하는 Client는 CAS(Common Application Server)라는 것을 거치게 되어 있는데, 이 CAS보다 요청하는 Client의 갯수가 많을 때에는 항상 에러를 내는 것이 아니고, 이미 연결되어 있는 다른 Connection이 Transaction 중이 아니라면 그 Connection의 Statement자원을 해제시켜 버리고 가져와서 쓸 수 도 있다고 합니다. 이것을 막는 설정이 KEEP_CONNECTION이라는 속성입니다. CUBRID Manager Client프로그램에서 바꿀 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/cubrid-keep-config.jpg&quot; alt=&quot;Cubrid설정.JPG&quot; title=&quot;Cubrid설정.JPG&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또, Cubrid는 하나의 Connection에서 동시에 두개의 Prepare를 오픈 할 수 없는 구조라고 하네요. (참고자료:http://cubrid.com/bbs/view.php?id=faq&amp;amp;no=318&amp;amp;category=3[] &lt;a href=&quot;http://cubrid.com/bbs/view.php?id=faq&amp;amp;no=318&amp;amp;category=3&quot; class=&quot;bare&quot;&gt;http://cubrid.com/bbs/view.php?id=faq&amp;amp;no=318&amp;amp;category=3&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 특징들을 보면, Cubrid는 Client의 명시적인 요청이 없이도 자원을 최대한 빨리 해제시켜 버리는 것이 사상이 아닌가 하는 생각도 듭니다. 어쩌면  제대로 자원 해제를 못하는 JDBC 코드가 많았던 시절에 그로 인해 생기는 문제들을 대응하면서 생긴 구조일 것 같다는 추측도 해봅니다.(참고: &lt;a href=&quot;http://blog.benelog.net/1898928&quot;&gt;JDBC에서 Connection, Statement,ResultSet의 close&lt;/a&gt; ) 그러나 대부분 프레임웍 기반의 개발을 하고 필요할 때 자원을 close시켜주는 코드들이 정착되고 있는 지금 시점에서는 큰 의미가 없는 특징이고, 오히려 다른 DB나 JDBC명세에 없는 동작으로 인한 혼란만 불러 일으킨다고 생각됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그래서 결론은 Cubrid에서 개발자의 의도보다 Statement를 먼저 해제시킬 수 있는 기능을 안 쓰려면 STATEMENT_POOLING과 KEEP_CONNECTION속성을 ON으로 하면 된다는 것입니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring batch 국내 자료링크, 그리고 KSUG 연재</title>
      <link>https://blog.benelog.net//2005178.html</link>
      <pubDate>Mon, 4 Aug 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">2005178.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(수정이력: 2008.01.19 - 스프링배치 관련 링크들은 &lt;a href=&quot;http://www.ksug.org/94로&quot; class=&quot;bare&quot;&gt;http://www.ksug.org/94로&lt;/a&gt; 옮기고, 최근 자료를 포함시켰습니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;7월31일에 Spring Batch 1.1.1이 나왔군요. 버그수정 외에 특별한  기능의 update 사안은 없기는 합니다만, 진도가 빠른 느낌입니다. ( &lt;a href=&quot;http://www.springframework.org/node/722&quot; class=&quot;bare&quot;&gt;http://www.springframework.org/node/722&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring batch에 대한 국내 자료도 점점 많이 생겨나고 있습니다.  &lt;a href=&quot;http://www.ksug.org/&quot;&gt;KSUG&lt;/a&gt;(한국 스프링사용자모임)에서 만난 분들 중 Spring batch에 관심을 가지고 계신 분들이 몇 분 계셔서, 새로운 자료들도 속속 올라올 것 같군요. 그런 분들과 정보를 주고 받을 수 있으니, 포럼(&lt;a href=&quot;http://forum.ksug.org&quot; class=&quot;bare&quot;&gt;http://forum.ksug.org&lt;/a&gt;) 개설 등 최근 &lt;a href=&quot;http://www.ksug.org/&quot;&gt;KSUG&lt;/a&gt;의 변화에 따른 수혜를 제가 많이 받고 있다고 느껴집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;스프링배치 국내 자료 모음 - &lt;a href=&quot;http://www.ksug.org/94&quot;&gt;http://www.ksug.org/94&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 제가 마이크로소프트웨어지에 연재하고 있는 글도 시일이 좀 지난 기사에 한해서 담당기자분의 허락을 얻어 &lt;a href=&quot;http://www.ksug.org/&quot;&gt;KSUG&lt;/a&gt;의 블로그에 올리고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 잡지에 나왔던 내용이지만, 인쇄되어 나올 원고를 쓸 때와는 달리 링크와 인용을 더 편하게 할 수 있기에 웹사이트에 올릴 수 있는 기회가 온 것을 반가워하고 있습니다. 연재용 원고를 쓸 때는 참조자료의 URL이 들어가면 긴 주소를 독자들이 직접 쳐서 웹사이트에 들어가 볼까 하는 의구심도 들고, 주소가 차지하는 지면 공간도 많고 해서 사소한 것까지 웹사이트 주소를 쓰기가 망설여 지더군요. 웹에 올릴 때는 API문서 등 사소한  링크도 다 넣을 수 있어서 속이 후련했습니다. 그리고 티스토리에서 제공되는 &apos;미주&amp;#8217;를 다는 기능도 정말 마음에 듭니다. 개인블로그도 이글루스에서 이사가고 싶은 마음이 생길 정도입니다;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 연재 내용을 포함한 스프링에 대한 모든 질문과 토론은 &lt;a href=&quot;http://forum.ksug.org/&quot;&gt;http://forum.ksug.org&lt;/a&gt;을 통해서 하시면 됩니다~!&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Apache commons DbUtils 활용하기</title>
      <link>https://blog.benelog.net//1978345.html</link>
      <pubDate>Tue, 15 Jul 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1978345.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개발을 하다보면 간단한 화면 1~2개만 독립적으로 돌아가는 웹애플리케이션을 만들 때도 있습니다. 예를 들면 로그조회 프로그램 같은 것들이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 곳에는 Hibernate나 iBatis를 쓰기에는 너무 거창하다는 느낌이 들기도 합니다. 그렇다고 JDBC로 날코딩하기는 성가실때, 이럴 때는 &lt;a href=&quot;http://commons.apache.org/dbutils/&quot;&gt;Apache Commons DbUtils&lt;/a&gt;를 써볼만 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;다운로드 : &lt;a href=&quot;http://commons.apache.org/downloads/download_dbutils.cgi&quot; class=&quot;bare&quot;&gt;http://commons.apache.org/downloads/download_dbutils.cgi&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API 문서 : &lt;a href=&quot;http://commons.apache.org/dbutils/apidocs/index.html&quot; class=&quot;bare&quot;&gt;http://commons.apache.org/dbutils/apidocs/index.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.benelog.net/1898928&quot;&gt;JDBC에서 Connection, Statement,ResultSet의 close&lt;/a&gt; 글에 나온 것처럼 Connection을 닫는 번거로운 처리가 DbUtils.closeQuietly(con);로 끝나는 것만 해도 상당히 편합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아래 예제는  DBUtils + JSTL로 간단한 조회화면을 만들어 본 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(Connection은 실무에서는 DataSource를 통해 얻어와야합니다.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;jsp&quot;&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=EUC-KR&quot;
    pageEncoding=&quot;EUC-KR&quot;%&amp;gt;
&amp;lt;%@ page import = &quot;java.sql.*&quot; %&amp;gt;
&amp;lt;%@ page import = &quot;java.util.Properties&quot; %&amp;gt;
&amp;lt;%@ page import = &quot;org.apache.commons.dbutils.DbUtils&quot; %&amp;gt;
&amp;lt;%@ page import = &quot;org.apache.commons.dbutils.QueryRunner&quot; %&amp;gt;
&amp;lt;%@ page import = &quot; org.apache.commons.dbutils.handlers.MapListHandler&quot; %&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;%!
  private static final String SELECT_STMT =
                  &quot;SELECT id, name, email, cell_phone_number FROM quiz_user&quot;;
%&amp;gt;
&amp;lt;%
 String url = &quot;jdbc:hsqldb:hsql://localhost/sampledb&quot;;
 Properties prop = new Properties();
 prop.put(&quot;user&quot;,&quot;sa&quot;);
 prop.put(&quot;password&quot;,&quot;&quot;);
 Connection con =  null;
 try\{
  Class.forName (&quot;org.hsqldb.jdbcDriver&quot;);
  con = DriverManager.getConnection(url,prop);
        QueryRunner runner = new QueryRunner();
        Object resultList = runner.query(con,SELECT_STMT, new MapListHandler());
        request.setAttribute(&quot;list&quot;,resultList);
   } catch (SQLException ex) \{

      throw new RuntimeException(ex);
   } finally \{
     DbUtils.closeQuietly(con);
   }
 %&amp;gt;
&amp;lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=EUC-KR&quot;&amp;gt;
&amp;lt;title&amp;gt;사용자&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;사용자  조회&amp;lt;/h1&amp;gt;
  &amp;lt;h2&amp;gt;사용자  목록&amp;lt;/h2&amp;gt;
  &amp;lt;table class=&quot;list&quot;&amp;gt;
   &amp;lt;tr&amp;gt;
    &amp;lt;th&amp;gt;id&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;이름&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;전화번호&amp;lt;/th&amp;gt; &amp;lt;th&amp;gt;이메일&amp;lt;/th&amp;gt;
 &amp;lt;/tr&amp;gt;
    &amp;lt;c:forEach var=&quot;item&quot; items=&quot;$\{list}&quot; varStatus=&quot;status&quot;&amp;gt;
 &amp;lt;tr&amp;gt;
  &amp;lt;td&amp;gt;$\{item.id}&amp;lt;/td&amp;gt;
  &amp;lt;td align=&quot;center&quot;&amp;gt;$\{item.name}&amp;lt;/td&amp;gt;
  &amp;lt;td&amp;gt;$\{item.cell_phone_number}&amp;lt;/td&amp;gt;
  &amp;lt;td align=&quot;center&quot;&amp;gt;$\{item.email}&amp;lt;/td&amp;gt;
 &amp;lt;/tr&amp;gt;
 &amp;lt;/c:forEach&amp;gt;
  &amp;lt;/table&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;몇 년전에 DbUtils와 비슷한 클래스를 만든 적이 있었는데, 그때도 좀 찾아볼 걸 그랬나봅니다. 그러고 보면 저도 Apache Commons에 이미 있는 것을 많이도 만들어본 삽질의 시간들을 겪었었습니다. 신입 때 &lt;a href=&quot;http://commons.apache.org/beanutils/&quot;&gt;commons beanutils&lt;/a&gt;하고 &lt;a href=&quot;http://commons.apache.org/io/&quot;&gt;commons io&lt;/a&gt;에 포함된 것 비슷한 유틸리티 만들어 놓고 혼자서 뿌듯해 했었죠 -_-;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/levin01/100011050694&quot;&gt;Commons-DbUtils&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/webman/30000419500&quot;&gt;dbutils 활용방법&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>이미 35개 이상의 Accenture 고객사에 Spring batch가 적용되고 있다</title>
      <link>https://blog.benelog.net//1970645.html</link>
      <pubDate>Thu, 10 Jul 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1970645.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring batch 프레임웍은  스프링 프레임웍의 본부인 SpringSource와 컨설팅 회사 Accenture가 협업해서 개발하고 있습니다. SpringSource의 기술력과 Accenture의 현장경험이 합쳐진 것이죠. 작년 5월에 국내에도 이 소식이 보도된 기사가 있군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://news.naver.com/main/read.nhn?mode=LSD&amp;amp;mid=sec&amp;amp;sid1=105&amp;amp;oid=092&amp;amp;aid=0000013437&quot;&gt;엑센추어, 오픈소스 프로젝트 「스프링 배치」참가&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://forum.springframework.org/forumdisplay.php?f=41&quot;&gt;Spring batch 포럼&lt;/a&gt;에서 보면 이미 현장에서 적용하고 있는 사람들이 많아 보이기는 해도, 아직 얼마나 쓰이고 있는지 짐작하기는 어려웠습니다. 그런데, 얼마전에 Accenture에서 내놓은 기사에 따르면 이미 35개 이상의 고객사에서 활용되고 있다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://accenture.tekgroup.com/article_display.cfm?article_id=4703&quot;&gt;Accenture and SpringSource Team to Deliver Production Version of Open Source Framework for Batch Processing&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 5월에 Spring batch에 대해서 공개 세미나에서 발표를 한 적이 있었습니다. 그 때 받은 질문 중에 하나가 &apos;금융권 등에서는 민감하고 고성능이 필요한 프로그램들이 배치로 돌고 있고, DB의 Stored procedure로 되어 있는 것이 많다. 그런 것들이 다 SpringBatch로 가능하겠느냐&amp;#8217;는 것이였습니다. 그 때 저는 &apos;성능에 대해서는 구체적으로 나온 자료가 없다. DB를 많이 쓰는 작업이라면 DB내부에서 돌아가는 Stored Procedure가 더 빠를 가능성이 크다. 그러나 배치 애플리케이션의 프로그래밍에서 추구할 우선적인 가치는 업무의 특성에 따라 다를 것이다. 그것이 성능일 때도 있고, 생산성이나 유지보수성일 때도 있다.  빠른 성능이 절대적으로 중요한 프로그램이 Spring batch의 모듈을 썼을 때 그 기대치가 안 나온다면 Store procedure로 주요부분을 짜고 그것을 Spring batch 내부에서 호출하는 방법도 가능하다.&apos;라고 대답을 했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 때 위의 기사의 사례들을 알았다면 &apos;이미 Accenture의 많은 고객사에서 사용하고 있는 것으로 보아 기업환경의 실무에서 충분히 사용 가능한 수준이라고 말할 수 있다.&apos;라는 말을 덧붙일수 있었을 건데 아쉽군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우리나라의 SI에서도 적용될만한 곳이 분명 있을 것입니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring 3.0에 대한 소식</title>
      <link>https://blog.benelog.net//1937831.html</link>
      <pubDate>Tue, 17 Jun 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1937831.html</guid>
      	<description>
	&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.springify.com/archives/15&quot;&gt;Live from SpringOne 2008: Collected bits about Spring 3.0&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;벌써 3.0에 대한 이야기가 나오고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;잘 하면 2008년 4분기 정도에 GA(General Availability) release(일반사용 가능 버전)가 나온다고 하는군요. Spring Core 모듈이 Java 5에 대해 본격적으로 의존하고 이제 더 이상  Java 1.4에서는 돌아갈 수 없다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 어느 정도 윤곽이 잡혀있듯이,  Spring MVC에서 REST에 대한 full scale 지원이 포함된다는군요. 그리고  Spring batch에서 지원되었던 repeat, retry, resume의 개념들과  Spring WS의 &lt;a href=&quot;http://static.springframework.org/spring-ws/site/reference/html/oxm.html&quot;&gt;Spring OXM&lt;/a&gt; 등이 보다 상위 모듈에서 포함되어서 제공될지도 모르겠습니다. 또, Junit 3.8 기반의 통합테스트 클래스들은 deprecated로 표시될 것 같습니다. (아마도 &lt;a href=&quot;http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/test/AbstractSingleSpringContextTests.html&quot;&gt;AbstractSingleSpringContextTests&lt;/a&gt;같은 클래스들을 이야기하는 것이겠죠?)&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Q33N의 비밀 - 뉴욕의 프로그래머 중</title>
      <link>https://blog.benelog.net//1936267.html</link>
      <pubDate>Mon, 16 Jun 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1936267.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;존경하는 임백준님의 책 &apos;&lt;a href=&quot;http://blog.hanb.co.kr/11&quot;&gt;뉴욕의 프로그래머&lt;/a&gt;&apos;에 있는 내용입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;노트패드로 할 수 있는 일이 생각보다 많다는 사실을 알고들 있는지?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어느 날 신나게 구라를 풀던 콜린이 말했다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;쌍 둥이 빌딩이 무너졌을 때 건물을 들이받은 비행기의 번호가 Q33N이었다는 것을 기억하는 사람은 없겠지만, 아무튼 그렇거든. 노트패드를 열고 Q33N을 입력해봐. 그리고 폰트 크기는 72로 최대한 키우고. 그 다음 글자체를 Wingdings로 선택하라고. 화면에 뭐가 나타나는지 보라고.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;story 3. &quot;무정부주의자 콜린&quot; 중에서&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전 책에서 이 부분을 읽자 마자 따라서 해봤고, 화면에 나온 결과를 보고 정말 신기하다고 느꼈죠. windows개발자가 몰래 911를 추도하기 위해서 넣은 것일 거라고 생각했었습니다. 나아가서 집에 있는 윈도우 2000이 깔린 컴퓨터에서도 되는 걸 보고 MS가 911이후에 2000버전의 노트패드에도 패치를 한건지, 아니면 개발자 중에 놀라운 예언가가 있었던것일까 하는 상상도 했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그.러.나.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;혹시나 해서 찾아봤더니, 실제의 비행기 번호는 그와 전혀 다른 번호였고, 위의 이야기는 정말로 &apos;구라&amp;#8217;였습니다. 원래 폰트가 그렇게 생겨먹은 겁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.hoax-slayer.com/wingdings-911.html&quot; class=&quot;bare&quot;&gt;http://www.hoax-slayer.com/wingdings-911.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음 링크의 아래쯤에 가면 &apos;Q33NY&amp;#8217;라는 문자열로 그런 소문이 도는데 사실이 아니라고 밝히고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.hoax-slayer.com/wingdings-911.html&quot;&gt;http://www.hoax-slayer.com/wingdings-911.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;네이버의 지식인에서도 비슷한 질문들이 오갔고, 여기서보니 호기심 천국에도 옛날에 나왔다고 하는군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://kin.naver.com/detail/detail.php?d1id=1&amp;amp;dir_id=106&amp;amp;eid=mEFZPG/iLDaISzjuCO7V4mXyyWV1VCWm&amp;amp;qb=cTMzbnk=&quot; class=&quot;bare&quot;&gt;http://kin.naver.com/detail/detail.php?d1id=1&amp;amp;dir_id=106&amp;amp;eid=mEFZPG/iLDaISzjuCO7V4mXyyWV1VCWm&amp;amp;qb=cTMzbnk=&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원래 저는 의심이 많은 편인데, 이번에는 완전 속을 뻔 했습니다 ^^; 아마 임백준님도 사실이 아니라는 것을 아시면서도 재미로 책에 넣으신거겠죠? ^^&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Spring batch와 Spring integration의 만남</title>
      <link>https://blog.benelog.net//1935914.html</link>
      <pubDate>Mon, 16 Jun 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1935914.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring portfolio의 일부 중 최근에 관심을 제가 관심을 가지고 있는  Spring batch와 Spring integration을 연결한 프로토타입 코드들이 나왔습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring batch form에 최근에 올라온 글입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://forum.springframework.org/showthread.php?t=55863&quot;&gt;Spring Integration Prototype&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 6월 18일에 이 것을 주제로 한 온라인 세미나 같은것이 있나 보네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.springframework.org/node/687&quot;&gt;Webinar: Enterprise Integration with Spring Batch&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 코드들은 Java 5를 사용하기 때문에 Spring batch 1.1 정식 배포판에는 들어가지 않을거라고 합니다. Spring batch 프로젝트에서는 2.0전까지 Java5 으로 가지는 않을거라고 하네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://zepag.blogspot.com/2008/06/springone08-spring-batch.html&quot;&gt;SpringOne 08의 Spring batch 주제의 발표&lt;/a&gt;에서도 관련 내용이 들어갈 것 같네요. Spring batch와 Spring integration의 convergence가 있는 것으로 보인다는 언급이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.egloos.com/egloo/insert.php?eid=d0032573&quot; class=&quot;bare&quot;&gt;http://www.egloos.com/egloo/insert.php?eid=d0032573&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Batch와 Spring WS, Spring Integration은 얽히고 섞힌 관계라고 보입니다. Spring integration의 Web Service Adapters에서 Spring Web Service를 사용하고 있고, Spring batch에서도 XML파일 처리에 Spring WS의 일부인 &lt;a href=&quot;http://static.springframework.org/spring-ws/site/reference/html/oxm.html&quot;&gt;Spring OXM&lt;/a&gt;를 통한 위임처리를 할 수 있습니다. 그리고 위와 같이 Spring integration는 Spring Batch도 연결시켰군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring Batch의 Job은 Spring Application Platform 통해서 OSGi 번들로도  실행가능하게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.springsource.com/main/2008/05/30/running-a-spring-batch-job-in-the-springsource-aplication-platform/&quot;&gt;Running a Spring Batch Job in The SpringSource Application Platform&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;향후 버전에서는 더욱 더 배치에 특화된 기능들이 추가될 것이라고 하네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring batch + Spring Integration + Spring WS + Spring Application Platform.. 그 미래가 어떻게 될지 정말 기대가 됩니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Effective Java 2nd Edition</title>
      <link>https://blog.benelog.net//1912122.html</link>
      <pubDate>Fri, 30 May 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1912122.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Amazon에서 예약주문 단계에서 판매상태로 바뀌었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.amazon.com/Effective-Java-2nd-Joshua-Bloch/dp/0321356683/ref=pd_bbs_sr_1?ie=UTF8&amp;amp;s=books&amp;amp;qid=1212115691&amp;amp;sr=8-1&quot;&gt;http://www.amazon.com/Effective-Java-2nd-Joshua-Bloch/dp/0321356683/ref=pd_bbs_sr_1?ie=UTF8&amp;amp;s=books&amp;amp;qid=1212115691&amp;amp;sr=8-1&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;회사에서 Yes24에서 책을 주문할 수 있는 쿠폰을 주기 때문에 저는 공짜로 보기 위해서 거기서 주문할 수 있을 때까지 기다리고 있는 중입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Joshua Bloch과의 인터뷰를 보니 역시 Generics등 Java5 이후의 주제들에 대해서 내용이 보강되었다고 하네요. 정확히는 21개의 항목과 83페이지가 늘어났다고 합니다. 기대가 되는군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.infoq.com/articles/bloch-effective-java-2e&quot;&gt;Book Excerpt and Interview: Effective Java, Second Edition&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;더불어 121페이지의 예제코드의 실수도 언급되어 있길래 옮겨봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;지금 인쇄판에 들어간 코드&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static Object reduce(List list, Function f, Object initVal) {
    Object[] snapshot = list.toArray(); // Locks list internally
    Object result = initVal;
    for (Object o : list) // 오류 부분
        result = f.apply(result, o);
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;수정된 내용&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;static Object reduce(List list, Function f, Object initVal) {
    Object[] snapshot = list.toArray(); // Locks list internally
    Object result = initVal;
    for (Object o : snapshot) // 수정된 부분 (list 대신 snapshot을 참조)
        result = f.apply(result, o);
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Joshua Bloch의 말에 따르면 다음에 찍는 인쇄판에서는 수정을 하겠지만 현재 시중에 나가는 판에는 이 오류가 포함이 되어 있다고 합니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java에서 Checked Exception은 언제 써야 하는가?</title>
      <link>https://blog.benelog.net//1901121.html</link>
      <pubDate>Fri, 23 May 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1901121.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 의 Exception 처리는 C++로부터 도입되었지만 checked exception은 Java만의 독특한 특징입니다. 아시다시피, 컴파일러가 exception을 꼭 처리해라고 강요하는 것이죠.  이것은  Java 이후에 설계된 언어인 C#이나 루비에도 채택되지 않았습니다. 즉 Java 이외의 다른 언어들의 Exception 처리 방식은 Java의 unchecked exception과 동일한 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java의 초기에는 checked Exception을 사용하라고 권장했지만, 지금은 많은 반론이 제기되고 있습니다. 극단적으로 Java언어에서checked Exception 도입 자체가 실패라고 주장하는 사람도 많습니다. Thinking in Java의 저자인 Bruce Eckel도 그 중 한 사람입니다.  Spring framework의 아버지 Rod Johnson도 Checked Exception이 쓰여야 할 때도 있지만 그 것이 과도하게 선호되어 온 것은 지적하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;어쨓든 Exception 부분은 Java로 API 설계와 코딩를 할 때 가장 어려운 부분이라고 느껴집니다. Java 아키텍트와 개발팀의 실력을 측정하는 좋은 방법은 그들이 만든 Exception 처리 코드를 보라는 말까지 있으니까요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;exception_처리_방식_참고자료&quot;&gt;Exception 처리 방식 참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mgp/book-notes/blob/master/effective-java-2nd-edition.markdown#chapter-9-exceptions&quot;&gt;Effective Java 2nd Edition Chapter 9 (Item 57 ~ 65)&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-57-use-exceptions-only-for.html&quot;&gt;Item 57&lt;/a&gt; : 예외는 예외상황에서만 써야 한다. 예외를 생성하고 던지고 잡는 것은 비용이 많이 드는 작업이고 JVM의 최적화 대상에서 빠질 수 있다. 프로그램 흐름을 예외로 제어하려 하면 안된다. 좋은 API는 클라이언트가 프로그램 흐름을 제어할 때 예외를 쓸 수 밖에 없도록 만들지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-58-use-checked-exceptions-for.html&quot;&gt;Item 58&lt;/a&gt;: 복구 가능한 조건에 Checked Exception을, 프로그래밍 에러에 RuntimeException을써라. Error의 하위 클래스는 만들지 말고 처리하지 않는 예외는 모두 RuntimeException의 하위클래스이어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/blog-post_07.html&quot;&gt;Item 59&lt;/a&gt; : Checked Exception은 꼭 필요할 때만 던져라. catch절에서 특별한 할 일이 없이 없는 API를 checked exception으로 처리하는 것은 프로그램만 더 복잡하게 만들 뿐이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-60-favor-use-of-standard.html&quot;&gt;Item 60&lt;/a&gt; : 표준예외를 선호하라. IllegalArgumentException, IllegalStatementException, UnsupportedOperationException, ConcurrentModificationException 등 Java의 표준예외를 활용해라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-61-throw-exceptions-appropriate-to.html&quot;&gt;Item 61&lt;/a&gt; : 예외를 적절하게 추상화 하라. 높은 계층에서 낮은 계층의 예외를 잡아서 높은 계층의 추상화 수준에 맞게 변환해서 던져야 한다. 예외변환(Exception translation) 패턴은 하위 레벨에 영향받지 않는 Exception 전파에 유리하지만 너무 남용하지는 마라. 가능하면 low-level exception이 없이 성공하도록 유도하는 것이 바람직하다. Exception chaining은 적절한 변환을 하면서도 세부 원인을 보존하는 장점이 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-62-document-all-exceptions-thrown.html&quot;&gt;Item 62&lt;/a&gt; : 메서드가 던지는 모든 예외를 명세문서에 기술하라 Checked exception은 메서드 선언부에 하나씩 선언하고, @throws 태그를 써서 모든 예외가 발생하느 상황을 정확하게 문서화하라. 단지 귀찮다는 이유만으로, 공통 상위타입으로 예외를 던지려 하지 마라. unchecked exception은 @throws 태그를 써서 명세문서에 기술하지만, 메서드 선언의 throws 절에는 나타나지 말아야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-63-include-failure-capture.html&quot;&gt;Item 63&lt;/a&gt; : 실패에 대한 자세한 정보를 상세 메시지에 담아라 실패원인을 포착하려면, 예외의 문자열 표현에 반드시 예외 발생에 영향을 준 모든 필드와 인자의 값이 들어 있어야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-64-strive-for-failure-atomicity.html&quot;&gt;Item 64&lt;/a&gt; : 실패 원자성을 얻기위해 노력하라. 메서드 호출이 실패하더라도 객체상태는 메서드 호출 전과 같아야 한다. 오류(error)는 예외(exception)와 달리 보통 복구할수 없기 때문에 오류가 발생했을 때 실패 원자성을달성하기 위해 애쓸 필요가 없다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://jtechies.blogspot.kr/2012/07/item-65-dont-ignore-exceptions.html&quot;&gt;Item 65&lt;/a&gt; : 예외를 잡아서 버리지 마라. 빈 catch block은 &quot;예외 사항을 처리하라&quot;라고 알려주는 예외의 존재 이유 자체를 짓밟는 것이다. catch block 안에서 정말 아무 것도 할 것이 없다면, 최소한 왜 예외를 잡아서 처리하지 않고 버리는지 그 이유라도 주석으로 달아 놓아야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.amazon.ca/Expert-One-One-Design-Development/dp/customer-reviews/0764543857&quot;&gt;Expert One-on-One J2EE Design and Development&lt;/a&gt; 중 Chapter 4 Design Techniques and Coding Standards for J2EE Projects, Exception Handling 부분&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;alternative return value가 있는 경우에는 Checked exception&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;data connection 생성 실패와 같이 뭔가 크게 잘못 되고 있어서 호출한 쪽에서 아무도 이를 처리할 수 없을 때는 Runtime exception.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;소수의 호출자만이 Exception을 받아서 처리해야 할 때도 Runtime exception.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;불명확하면 Runtime exception.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/haruma95/80050223953&quot;&gt;checked Exception과 Runtime exception&lt;/a&gt; 페이지에서 일부 내용이 번역되어 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=1943792&amp;amp;CategoryNumber=001001003016003012&quot;&gt;Spring 프레임웍크 워크북&lt;/a&gt; (박재성 저) 88쪽에도 인용되어 있는 원칙입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Barry Ruzek의  &lt;a href=&quot;http://dev2dev.bea.com/pub/a/2006/11/effective-exceptions.html&quot;&gt;EFFECTIVE JAVA EXCEPTIONS&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Fault Handling(Unplanned condition)에는 RuntimeException, Contingency(Expected condition, alternative method result)에는 return type, exception, error code 등의 전략을 사용&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fault를 한곳에서 잡아서 처리하는 Fault barrier pattern 사용 권장. Struts라면 org.apache.struts.action.ExceptionHandler, SpringMVC라면 SimpleMappingExceptionResolver&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AOP 적용이 도움이 될 수도 있음.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;황상철님의 블로그 &lt;a href=&quot;http://moai.tistory.com/322&quot;&gt;Effective Java Exceptions 발표자료&lt;/a&gt; 페이지에서 이 내용이 요약된 pdf 파일을 받을 수 있습니다. (&lt;a href=&quot;http://moai.tistory.com/attachment/dk180000000000.pdf&quot;&gt;effective java exceptions.pdf&lt;/a&gt; )&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Jim Cushing  &lt;a href=&quot;http://today.java.net/pub/a/today/2003/12/04/exceptions.html&quot;&gt;Three Rules for Effective Exception Handling&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Be specific, Throw Early, Catch Late의 3가지 원칙을 제시하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.manageability.org/blog/stuff/exceptional-exception-handling-techniques/view&quot;&gt;13 Exceptional Exception Handling Techniques&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Checked Exception을 RuntimeException으로 감싸기, throws 절에 RuntimeException이라도 선언해주기 등의 기법을 추천하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gunjan Doshi의 &lt;a href=&quot;http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html&quot;&gt;Best Practices for Exception Handling&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Client code가 할 일이 있을 때는 checked, 없을 때는 unchecked. progamming error에는 unchecked exception.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;적절한 캡슐화. 비지니스 layer에서 SqlException을 던지지 말것.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rob Walling의 Exception Handling에 관한 글 &lt;a href=&quot;http://www.softwarebyrob.com/2007/08/27/two-fundamental-no-frills-square-one-rules-exception-handling/&quot;&gt;The Two Fundamental, No Frills, Square One Rules of Exception Handling&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;에러메시지에 도움이 되는 정보를 더할 수 없다면, Exception을 잡지마라, 2. Exception을 잡았으면 기록하라.&quot; 두가지 원칙을 말하고제시하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Alan Griffiths의 &lt;a href=&quot;http://www.octopull.demon.co.uk/java/ExceptionalJava.html&quot;&gt;Exceptional Java&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;이 주제에 대해 비교적 초기에 나온 글로써, 다른 글에도 많이 인용되고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exception 처리에 대한 전통적인 원칙이 캡슐화 저해, 정보손실, 정보 과적의 문제를 일으킨다고 이야기합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;public 메서드에서 던지는 Exception은 해당 패키지에 소속된 클래스일것, 다른 패키지에서는 이를 부를 때를 Exception을 전파시키지 말고 그 패키지의 Exception으로 감쌀 것을 추천하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.onjava.com/pub/a/onjava/2006/01/11/exception-handling-framework-for-j2ee.html?page=1&quot;&gt;An Exception Handling Framework for J2EE Applications&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;J2EE application에서의 Exception처리 전략에 대해서 설명하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;checked_exception에_대한_부정적_견해를_정리한_글&quot;&gt;Checked Exception에 대한 부정적 견해를 정리한 글&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Brian Goetz의 글: &lt;a href=&quot;http://www.ibm.com/developerworks/java/library/j-jtp05254.html&quot;&gt;Java theory and practice: The exceptions debate&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bruce Eckel, Rod Johnson, Joshua Bloch등의 주장을 정리해 놓은 글입니다. Checked Exception이 상세한 구현을 부적절하게 노출함,불안정한 메서드 시그너처,읽기힘든 코드,Exception 삼키기, 너무 많은 Exception wrapping의 문제점 있는 것을 나열하고 있습니다. unchecked exception은 Documentation이 더욱 중요하다고 강조하고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Rod Waldhoff의 글 : &lt;a href=&quot;http://radio.weblogs.com/0122027/stories/2003/04/01/JavasCheckedExceptionsWereAMistake.html&quot;&gt;Java&amp;#8217;s checked exceptions were a mistake&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Java의 Exception Handling은 실패한 실험이라고 주장하는 내용입니다. Checked Exception방식은 일부 low level에서만 의미가 있다고 말합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bruce Eckel의 견해(Thinking in Java의 저자): &lt;a href=&quot;http://www.mindview.net/Etc/Discussions/CheckedExceptions&quot;&gt;Does Java need Checked Exceptions?&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Error report 방식을 통일했다는 점에서는 의미가 있지만, 오히려 개발자들이 Exception을 그냥 삼키는 코드를 많이 짜게 하는 결과가 생겼다고 지적합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bill Venners(C#의 아키텍트)와 Bruce Eckel의 인터뷰 : &lt;a href=&quot;http://www.artima.com/intv/handcuffs.html&quot;&gt;The Trouble with Checked Exceptions&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;versioning, scalability의 문제 때문에 C#에  Checked Exception이 도입되지 않았다고 밝히고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;checked_exception에_대한_논의가_진행된_페이지&quot;&gt;Checked Exception에 대한 논의가 진행된 페이지&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.c2.com/cgi/wiki?JavaExceptionsAreParticularlyEvil&quot;&gt;Java Exceptions Are Particularly Evil&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.c2.com/cgi/wiki?CheckedExceptionsAreOfDubiousValue&quot;&gt;Checked Exceptions Are Of Dubious Value&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.c2.com/cgi/wiki?ExceptionTunneling&quot;&gt;Exception Tunneling&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.theserverside.com/news/thread.tss?thread_id=19192&quot;&gt;Opinion: The Eternal Debate on Checked Exceptions&lt;/a&gt;# : theserverside에서 벌어진 논쟁&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;이클립스_exception처리_코드_템플릿_관련_자료&quot;&gt;이클립스 Exception처리 코드 템플릿 관련 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://lastmind.net/blog/2007/11/exception-handling.html&quot;&gt;Exception Handling&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://whiteship.tistory.com/1612&quot;&gt;이클립스의 기본 try-catch 탬플릿 비추&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;기타_자료&quot;&gt;기타 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Exception-Handling Antipatterns : &lt;a href=&quot;http://today.java.net/pub/a/today/2006/04/06/exception-handling-antipatterns.html&quot; class=&quot;bare&quot;&gt;http://today.java.net/pub/a/today/2006/04/06/exception-handling-antipatterns.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Error handling, A Pattern Language : &lt;a href=&quot;http://www.objectarchitects.de/arcus/cookbook/exhandling/index.htm&quot; class=&quot;bare&quot;&gt;http://www.objectarchitects.de/arcus/cookbook/exhandling/index.htm&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Best practices in EJB exception handling :  &lt;a href=&quot;http://www-106.ibm.com/developerworks/java/library/j-ejbexcept.html&quot;&gt;http://www-106.ibm.com/developerworks/java/library/j-ejbexcept.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.javaworld.com/jw-07-1998/jw-07-techniques.html&quot;&gt;http://www.javaworld.com/jw-07-1998/jw-07-techniques.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.javaworld.com/jw-07-1998/jw-07-exceptions.html&quot;&gt;http://www.javaworld.com/jw-07-1998/jw-07-exceptions.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.javaworld.com/javaworld/jw-08-2001/jw-0803-exceptions.html&quot;&gt;http://www.javaworld.com/javaworld/jw-08-2001/jw-0803-exceptions.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.javaworld.com/javaworld/jw-09-2001/jw-0914-exceptions.html&quot;&gt;http://www.javaworld.com/javaworld/jw-09-2001/jw-0914-exceptions.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Beware the dangers of generic Exceptions : &lt;a href=&quot;http://www.javaworld.com/javaworld/jw-10-2003/jw-1003-generics.html&quot; class=&quot;bare&quot;&gt;http://www.javaworld.com/javaworld/jw-10-2003/jw-1003-generics.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.codemonkeyism.com/archives/2008/12/16/7-good-rules-to-log-exceptions/&quot; class=&quot;bare&quot;&gt;http://www.codemonkeyism.com/archives/2008/12/16/7-good-rules-to-log-exceptions/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://northconcepts.com/blog/2013/01/18/6-tips-to-improve-your-exception-handling/&quot; class=&quot;bare&quot;&gt;http://northconcepts.com/blog/2013/01/18/6-tips-to-improve-your-exception-handling/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>JDBC에서 Connection, Statement,ResultSet의 close</title>
      <link>https://blog.benelog.net//1898928.html</link>
      <pubDate>Wed, 21 May 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1898928.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이미 많이 알려진 내용이지만, 아직도 문제를 많이 일으키는 주제입니다. 그래서 보다 이 주제를 검색엔진에서 쉽게 찾을 수 있었으면 하는 마음에서 이 글을 정리해봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;  Connection conn = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null; // &amp;lt;---- !!!
  try{
     conn = ...&amp;lt;getConnection()&amp;gt;...;
     pstmt = conn.prepareStatement(&quot;select .....&quot;);
     rs = pstmt.executeQuery(); // &amp;lt;----- !!!
     while(rs.next()){
       ......
     }
  }  catch(Exception e){
     ....
  }  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( pstmt != null ) try{pstmt.close();}catch(Exception e){}
     if ( conn != null ) try{conn.close();}catch(Exception e){}

 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이것이 JDBC API 사용시에 권장되는 코딩방식입니다. 코드는 참조자료에 있는 이원영님의 글에서 인용했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDBC 스펙을 찾아보면 Statement가 닫힐 때 ResultSet은 닫히고, Connection이 닫히면 Statement도 닫힌다고 되어 있습니다. 하지만 Staement close 시에 Exception이 발생한다면 이것이 따로 Exception을 catch되지 않고서는 뒤에 Connection을 닫는 코드가 실행되지 않습니다. 그리고 Connection pool에서 얻어온 Connection객체는 connection.close()로 처리하는 것이 pool로의 반환을 의미하는 것이지 실제로 connetion을 close하는 것이 아니기 때문에 Statement까지 닫아준다고 장담할 수 없습니다. ResultSet의 경우도 WAS에도 제공하는 Statement cache 기능 때문에 명시적으로 close해주는 것이 확실한 자원해제를 보장할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;DBMS에서 &quot;maximum open cursor exceed !&quot; 나 &quot;Limit on number of statements exceeded &quot; 에러를 내고 있다면 위와 같이 코딩했는지 한번 확인해보시기 바랍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각 벤더별 드라이버의 구현이나 WAS의 Connection Pool의 구현등에 따라서 저 정도까지 안 해도 문제가 안 생길 수도 있습니다. 그리고 독립적으로 돌아가는 배치프로그램이나 커넥션풀을 쓰지 않는 경우에는 보다 덜 엄격해도 될 때도 있습니다.  그래도 어떠한 경우에도 안심하고 있을만한 코드는 위와 같은 구조입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;javaservice.net에서 이원영님이 처음에 이 문제에 대한 글을 쓰신것이 2000년 9월입니다. 그래서  많은 분들이 알고 계시지만 그래도 정말 반복적으로 만나게 되는 문제입니다.  저의 경험이 편향된지도 모르겠지만, 지금까지 제가 만났던 JDBC AP를 그대로 쓰는 개발팀은 세 팀이였었는데, 모두 이렇게 코딩하지 않을 경우 문제가 생길 가능성이 있다는 것을 모르고 있었습니다. 결국 그 중 한 팀은 시스템 전체를 몇 일동안 매시간마다 재부팅시키게 만들게 했었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;미국의 모 대형항공사의 예약시스템을 3시간동안 멈춘 코드도 위와 같은 방식을 따르지 않았었습니다. finally절이 다음과 같았다고 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;} finally{
    if (stmt!=null) stmt.close();
    if (conn!=null) conn.close();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 예약 시스템은 이중화된 DB로 구성되어 있었고, 그 DB들은 가상IP주소로 애플리케이션과 연결되어 있었습니다.  정기 점검을 위해 DB중 하나를 수동  fail-over 시키는 순간 내려간 DB의 JDBC연결에서 나온 statement객체의 close문장은 Exception을 일으켰습니다. 이 문장은 별도로 catch 되지 않았기 때문에 그 다음의 conn.close()는 실행되지 않았습니다. 결국 이 때문에 반환되지 않은 Connection 자원들로 인해 리소스 풀은 곧 바닥이 났습니다. 그 후에 새로 Connection을 얻고자 하는 다른 프로그램들은 블록되어서 전체 시스템을 멈추었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아마도 JDBC API를 쓰는 곳에는 언제나 생길 수 있는 문제일 것입니다. 좋은 API는 문서를 안 보고 자연스럽게 써도 사용하기 쉽고 문제를 안 일으키는 것일텐데, JDBC는 제대로 사용하기가 오히려 더 어려운 API입니다. 위의 항공사 사건 같이 전 세계에서 JDBC로 인해 야기된 장애,생산성 저하를 다 따져본다면, 가히 이 API가 인류에게 끼친 해악이 엄청나다는 생각까지도 듭니다.  요즘은 Framework 기반 개발로 JDBC를 직접 안 쓰는 것이 이런 점에서는 다행입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JDBC API에서 대표적으로 지적받는 문제점은 Checked Exception을 남발했다는 것입니다. catch 절에서 아무 것도 하지 않는 것은 바람직하지 않은 코딩이지만 JDBC API에서는 정말 할 것이 없습니다. 그래서 이런 문제점을 알고서 그 후에 나온 JDBC를 활용한 API들, Spring의 &lt;a href=&quot;http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/jdbc/core/JdbcTemplate.html&quot;&gt;JdbcTemplet&lt;/a&gt;, &lt;a href=&quot;http://www.hibernate.org/&quot;&gt;Hibernate&lt;/a&gt;의  &lt;a href=&quot;http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Query.html&quot;&gt;Query&lt;/a&gt; 인터페이스,  &lt;a href=&quot;http://en.wikipedia.org/wiki/Java_Persistence_API&quot;&gt;JPA&lt;/a&gt;의 &lt;a href=&quot;http://java.sun.com/javaee/5/docs/api/javax/persistence/Query.html&quot;&gt;Query&lt;/a&gt; 인터페이스, &lt;a href=&quot;http://java.sun.com/jdo/&quot;&gt;JDO&lt;/a&gt;의 &lt;a href=&quot;http://db.apache.org/jdo/api21/apidocs/javax/jdo/Query.html&quot;&gt;Query&lt;/a&gt; 인터페이스에서는 Checked Exception인 SqlException을 볼 수 없게 설계되어 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 Java6 이전의 JDBC에서는 접속에러, 쿼리에러, 제약조건 에러 등 다양한 원인으로 생기는 Exception을 SqlException 1개로 다 때우는 문제도 있었습니다. Spring에서는 이것을 더 섬세하게 구분한  Exception들을 정의를 하고 있습니다. &lt;a href=&quot;http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/dao/DataAccessException.html&quot;&gt;DataAccessException&lt;/a&gt;의 하위 클래스를 보면 &lt;a href=&quot;http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/dao/CleanupFailureDataAccessException.html&quot;&gt;CleanupFailureDataAccessException&lt;/a&gt;, &lt;a href=&quot;http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/dao/DataIntegrityViolationException.html&quot;&gt;DataIntegrityViolationException&lt;/a&gt;, &lt;a href=&quot;http://static.springframework.org/spring/docs/2.0.x/api/org/springframework/dao/DataRetrievalFailureException.html&quot;&gt;DataRetrievalFailureException&lt;/a&gt; 등이 보입니다. Java6에 포함된 JDBC 4.0에서는  &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/api/java/sql/SQLNonTransientException.html&quot;&gt;SQLNonTransientException&lt;/a&gt;, &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/api/java/sql/SQLRecoverableException.html&quot;&gt;SQLRecoverableException&lt;/a&gt;, &lt;a href=&quot;http://docs.oracle.com/javase/7/docs/api/java/sql/SQLTransientException.html&quot;&gt;SQLTransientException&lt;/a&gt; 등의 하위 클래스가 생겼고, ,Spring에서는 이런 클래스도 잘 인식해서 적절한 DataAccessException의 하위 클래스로 변환해줍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.benelog.net/1901121&quot;&gt;Java의 Checked Exception 처리 문제에 관한 글&lt;/a&gt;은 따로 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;미국_항공사_장애_사건_관련&quot;&gt;미국 항공사 장애 사건 관련&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2753365&amp;amp;CategoryNumber=001001003016002003&quot;&gt;Release It 릴리스 잇 : 성공적인 출시를 위한 소프트웨어 설계와 배치&lt;/a&gt;  (마이클 나이가드 저/신승환,정태중 역, 위키북스)&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;2장 사례연구 : 항공사를 정지시킨 예외(Exception) 사건&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;jdbc_관련&quot;&gt;JDBC 관련&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Javaservice.net에 이원영님이 올린 글&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;(죽은 링크) &lt;a href=&quot;http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&amp;amp;b=servlet&amp;amp;c=r_p&amp;amp;n=968185187&amp;amp;k=JDBC&amp;amp;d=tb&quot;&gt;서블렛 + JDBC 연동시 코딩 고려사항 -제1탄-&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;(죽은 링크) &lt;a href=&quot;http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&amp;amp;b=servlet&amp;amp;c=r_p&amp;amp;n=968522077&quot;&gt;서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현 시점에서 찾을수 있는 해당 글의 복사본&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://winmargo.tistory.com/46?category=428145&quot; class=&quot;bare&quot;&gt;https://winmargo.tistory.com/46?category=428145&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.zdnet.co.kr/view/?no=00000010048177&quot;&gt;서블렛 + JDBC 연동시 코딩 고려사항 4&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.zdnet.co.kr/view/?no=00000010048223&quot;&gt;서블렛 + JDBC 연동시 코딩 고려사항 6&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Digester를 이용한 Naver Open API Java Client 모듈</title>
      <link>https://blog.benelog.net//1891482.html</link>
      <pubDate>Fri, 16 May 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1891482.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;네이버 Open API Cafe에서 검색 API의 Java Client 모듈을 보게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://cafe.naver.com/ArticleRead.nhn?clubid=11906219&amp;amp;amp;page=1&amp;amp;amp;menuid=3&amp;amp;amp;boardtype=L&amp;amp;amp;articleid=1372&quot; class=&quot;bare&quot;&gt;http://cafe.naver.com/ArticleRead.nhn?clubid=11906219&amp;amp;amp;page=1&amp;amp;amp;menuid=3&amp;amp;amp;boardtype=L&amp;amp;amp;articleid=1372&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://insford.tistory.com/116&quot; class=&quot;bare&quot;&gt;http://insford.tistory.com/116&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 모듈을 참고해서 같은 역할을 하는 모듈을 다르게 구현해봤습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;특징은 아래와 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;RSS를 파싱하는 부분을 XML parsing API로 널리 알려진 Digester(&lt;a href=&quot;http://commons.apache.org/digester/)를&quot; class=&quot;bare&quot;&gt;http://commons.apache.org/digester/)를&lt;/a&gt; 사용했습니다. Digester의 예제 코드로 제공되는 RSS파싱모듈을 그대로 써서 짧은 코드로 파싱이 가능했습니다.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;API 문서 : (&lt;a href=&quot;http://commons.apache.org/digester/commons-digester-1.5/docs/api/org/apache/commons/digester/rss/RSSDigester.html&quot;&gt;RSSDigester&lt;/a&gt; , &lt;a href=&quot;http://commons.apache.org/digester/commons-digester-1.5/docs/api/org/apache/commons/digester/rss/Channel.html&quot;&gt;Channel&lt;/a&gt; , &lt;a href=&quot;http://commons.apache.org/digester/commons-digester-1.5/docs/api/org/apache/commons/digester/rss/Item.html&quot;&gt;Item&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;요청 파라미터를 담는 클래스를 따로 뺐습니다.  (RequestParameter.java) 이중 Target 값은 (blog, news 등 검색할 콘텐츠 유형을 선택하는 파라미터입니다.) enum으로 해서 정해진 값이 아닐 경우 compile이 안 되게 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;open API key값은 필수값이므로 OpenApiClient클래스의 생성자의 파라미터로 받았습니다. 대신 키 값이 없이 이 객체가 생성될 수 없도록 default 생성자는 private으로 돌려놨습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;첨부한 파일은 이클립스에서 Dynamic Web Project로 생성한 폴더를 압축한 것입니다. 테스트 실행 서버는 Tomcat 5.5를 사용했습니다. Eclipse WTP가 설치되어 있는 환경이면 실행이 가능합니다. 그리고 enum을 썼기에 Java5이상이어야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open API에 대한 자세한 사용법은 &lt;a href=&quot;http://openapi.naver.com/index.nhn&quot; class=&quot;bare&quot;&gt;http://openapi.naver.com/index.nhn&lt;/a&gt; 를 참조하시면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;소스코드&quot;&gt;소스코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;NaverSearchClient.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package openapiclient;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;

import org.apache.commons.digester.rss.Channel;
import org.apache.commons.digester.rss.RSSDigester;

public class NaverSearchClient {

    private static final String OPEN_API_URL = &quot;http://openapi.naver.com/search&quot;;
    private String key;

    @SuppressWarnings(&quot;unused&quot;)
    private NaverSearchClient(){};

    public NaverSearchClient(String key){
    this.key = key;
    }
    public Channel search(RequestParameter param) throws Exception{
        RSSDigester digester = new RSSDigester();
        URL requestUrl = getRequestUrl(param);
        InputStream is = requestUrl.openConnection().getInputStream();
     return (Channel) digester.parse(is);
    }

    private URL getRequestUrl(RequestParameter param) throws UnsupportedEncodingException, MalformedURLException {
        StringBuffer serverUrl = new StringBuffer(OPEN_API_URL);
        serverUrl.append(&quot;?target=&quot; + param.getTarget());
        serverUrl.append(&quot;&amp;amp;key=&quot; + key);
        serverUrl.append(&quot;&amp;amp;start=&quot; + param.getStart());
        serverUrl.append(&quot;&amp;amp;display=&quot; + param.getDisplay());
        serverUrl.append(&quot;&amp;amp;query=&quot; + URLEncoder.encode(param.getQuery(), &quot;UTF-8&quot;));
        if(param.getSort()!=null) serverUrl.append(&quot;&amp;amp;sort=&quot; + param.getSort());
        return new URL(serverUrl.toString());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;RequestParameter.java&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;package openapiclient;

public class RequestParameter {

 public enum Category{
  KIN,BLOG,CAFE,DOC,WEBKR,BOOK, SHOP, ENCYC,
  KRDIC, JPDIC, ENDIC, NEWS, LOCAL, VIDEO,IMAGE;
  public String toString(){
   return super.toString().toLowerCase();
  }
 }

 private Category target;
 private String sort;
 private int start;
 private int display;
 private String query;

 // getter and setters 생략
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;jsp에서_사용한_예제&quot;&gt;JSP에서 사용한 예제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JSTL을 함께 사용해서 찍어본 예제입니다. &lt;a href=&quot;http://openapi.naver.com/index.nhn&quot; class=&quot;bare&quot;&gt;http://openapi.naver.com/index.nhn&lt;/a&gt; 에 가셔서 API key를 발급 받으시고 소스 중간에 밑줄로 표시된 부분에 그 값을 넣으시고 돌려주시면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;jsp&quot;&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=EUC-KR&quot;   pageEncoding=&quot;EUC-KR&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;%@ page import=&quot;openapiclient.RequestParameter&quot; %&amp;gt;
&amp;lt;%@ page import=&quot;openapiclient.NaverSearchClient&quot; %&amp;gt;
&amp;lt;%@ page import=&quot;org.apache.commons.digester.rss.Channel&quot; %&amp;gt;
&amp;lt;%
 String KEY = &quot;????&quot;; // Open API key값을 넣으세요
 NaverSearchClient client = new NaverSearchClient(KEY);
 RequestParameter param = new RequestParameter();
 param.setDisplay(10);
 param.setStart(1);
 param.setQuery(&quot;미역국&quot;);
 param.setTarget(RequestParameter.Category.NEWS);
 Channel result = client.search(param);
 result.render(System.out); // 콘솔에 받아온 내용을 확인삼아 찍어봄
 request.setAttribute(&quot;result&quot;, result);
%&amp;gt;
&amp;lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD HTML 4.01 Transitional//EN&quot; &quot;http://www.w3.org/TR/html4/loose.dtd&quot;&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=EUC-KR&quot;&amp;gt;
&amp;lt;title&amp;gt;Naver Open API를 이용한 검색&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;c:forEach var=&apos;item&apos; items=&apos;${result.items}&apos;&amp;gt;
  &amp;lt;p&amp;gt;
  &amp;lt;a href=&quot;${item.link}&quot;&amp;gt; ${item.title} &amp;lt;/a&amp;gt; &amp;lt;br/&amp;gt;
  ${item.description}
  &amp;lt;/p&amp;gt;
&amp;lt;/c:forEach&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;실행결과화면&quot;&gt;실행결과화면&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/my-program/naver-open-api-client-in-eclipse.jpg&quot; alt=&quot;openApiClient.JPG&quot; title=&quot;openApiClient.JPG&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Batch job에서의 바람직한 트랜잭션 처리 방식</title>
      <link>https://blog.benelog.net//1889589.html</link>
      <pubDate>Thu, 15 May 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1889589.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;일반적인 온라인 애플리케이션에서는 사용자가 보내는 event 한번에 하나의 트랜잭션을 묶어서 처리합니다. 하지만 배치처리에서는 하나의 처리가 하나의 트랜잭션으로 관리되는 것은 때로는 문제가 될 여지가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;몇 만 건 이상의 행과 같이 대용량 데이터를 다루는 경우에는 roll back segment 같은 공간의 부족현상이 생길 위험이 있습니다. 그리고 상대적으로 긴 처리 시간동안 DB에 lock을 걸리게 해서 전체 시스템에 병목을 만들지도 모릅니다. 사용자가 많은 웹애플리케이션이 동시에 접근하는 DB에서 배치처리를 돌릴 때는 이를 더 염두에 두어야 할 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;또, 데이터가 건마다 독립성을 가지고 있는 경우라면 한 두건의 처리실패 때문에 앞에 성공한 건들도 다 Roll back이 되어버린다면 더 많은 건의 데이터가 의도하지 않은 상태로 오래 유지됩니다. 배치처리의 에러가 발생 즉시 해결되지 않을 수도 있기 때문에 때문에 업무 규칙상으로 가능한 경우라면 부분적으로 성공한 건이라도 DB에 반영되는 것이 좋습니다. 그렇지 않을 경우에 재시도시에 다시 모든 데이터를 처리해야 하니 그 처리시간이 더 길어지게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다고 해서 데이터 매 건마다 따로 트랜잭션 처리를 하는 것도 성능상 좋지 못합니다. 트랜잭션을 시작하고 끝낼 때 드는 비용도 감안해야 되기 때문입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;따라서 일정한 행 단위를 묶어서 처리를 하는 것이 배치처리에서는 더 바람직한 방식입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;WebSphere XD Compute Grid에서는 check-point algorithm을 time-based와 record-based 두 가지 방식으로 제공해서 이런 문제에 대한 해결책을 제시하고 있습니다.
Spring batch에서는 SimpleStepFactoryBean 클래스에서 commitInterval이라는 속성으로 주기적인 commit을 설정할 수 있습니다. 데이터 특성이나 업무규칙에 따라서 적절한 값을 트랜잭션 단위로 묶이는 건수를 지정할 수 있는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료_1&quot;&gt;참고자료 1&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.devx.com/Java/Article/20791/1763/page/1&quot;&gt;High-volume Transaction Processing in J2EE&lt;/a&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;세번째 페이지의 Job Partitioning and Chunking 단락에서 아래와 같이 설명하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Running your job as one long-lived transaction isn&amp;#8217;t a good idea. Long-lived transactions are more fragile than short-lived transactions. They are more susceptible to errors and DBMS locking problems because the locks are held longer. Smaller chunks are more reliable and less likely to exceed your connection and session timeouts.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료2&quot;&gt;참고자료2&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.speakeasy.org/~jwilton/oracle/dedicated-rbs.html&quot;&gt;Misconception: Big batch jobs should use a ‘dedicated’ rollback segment&lt;/a&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Oracle의 경우를 설명한 글입니다. DBA들이 보통 긴 transaction을 유지하는 batch job에 전용 role back segment를 할당하는데 실제로는 이것이 별도움이 못 된다고 말합니다. 결국 다른 session들이 사용할 수 있는 roll back segment를 사용하는 것은 똑같기 때문에 ORA-01555 에러를 유발할 수 있다고 합니다. 결국 보다 작은 transaction 단위를 가지고 가는 것이 근본적인 해결책입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 실패시 재시도를 위해서 지난 번 처리한 영역까지 표시하거나 앞에 성공한 건들을 다 지워줘야 하는 경우 때문에  큰 transaction을 가지고 가는 문제가 .생기는데, 재시도를 위한 기능을 트랜잭션에서 제공받기 전에 응용프로그램에서 적절한 로직으로 처리하는 것이 필요하다고 나와 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The problem that needs to be addressed in cases such as these is the design of the batch processes that require such a huge transaction.  Transactions in Oracle should normally be kept fairly short.  While it is undesirable to commit for every row processed (which will cause excessive redolog buffer flushing and high waits on “log file sync”), it makes sense to have batch processes commit for every few hundred rows processed.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Often the greatest barrier to changing batch jobs to commit continuously is failure tolerance.  If a batch job that commits continuously fails part way through, then there must be a way to restart that batch job where it left off, or clean up from the first attempt so that the job can be started over.  Whereas before this restart capability was provided by rolling back the large transaction, the proposed rollback-friendly model requires that the appropriate application logic be built into the batch processing software.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you allow your semi-frequently committing batch jobs to randomly select rollback segments like all the rest of the transactions in your system, you will be less likely to overwrite recently committed changes, since the burden of the batch transactions is spread around, rather than concentrated in a single rollback segment&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료3&quot;&gt;참고자료3&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ibm.com/developerworks/websphere/library/techarticles/0606_antani/0606_antani.html&quot;&gt;Solving Business Problems with WebSphere Extended Deployment&lt;/a&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;WebSphere XD에서 제공하고 있는 check-point algorithm에 대한 설명을 볼 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료4&quot;&gt;참고자료4&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://static.springframework.org/spring-batch/spring-batch-docs/reference/html/execution.html#d0e3602&quot;&gt;Spring batch documentation - 4.4.1.2. Configuring a CommitInterval&lt;/a&gt; :&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spring batch에서 주기적인 commit을 설정하는 방법입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>매쉬업(Mashup) 도구들</title>
      <link>https://blog.benelog.net//1857319.html</link>
      <pubDate>Wed, 23 Apr 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1857319.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;yahoo_pipe&quot;&gt;Yahoo Pipe&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pipes.yahoo.com/pipes/&quot; class=&quot;bare&quot;&gt;http://pipes.yahoo.com/pipes/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;설명 : &lt;a href=&quot;http://en.wikipedia.org/wiki/Yahoo_Pipes&quot; class=&quot;bare&quot;&gt;http://en.wikipedia.org/wiki/Yahoo_Pipes&lt;/a&gt; , &lt;a href=&quot;http://radar.oreilly.com/archives/2007/02/pipes-and-filters-for-the-inte.html&quot;&gt;Pipes and Filters for the Internet&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;예제&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://pipes.yahoo.com/pipes/pipe.info?_id=vvW1cD212xGMiR9aqu5lkA&quot;&gt; New York Times thru Flickr&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;사용법&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://usefulvideo.blogspot.com/2007/02/yahoo-pipes-tutorials.html&quot;&gt;Yahoo! Pipes tutorials&lt;/a&gt; &lt;a href=&quot;http://www.jumpcut.com/fullscreen?id=C086AA92568811DCAB02000423CF381C&amp;amp;amp;type=movie&quot;&gt;How to Translate a Feed Using Pipes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.jumpcut.com/fullscreen?id=594F555C568011DC9D24000423CEF5B0&amp;amp;amp;type=movie&quot;&gt;Learn How to Build a Pipe in Just a Few Minutes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://alexeysmirnov.name/blog/?page_id=198&quot;&gt;Alexey&amp;#8217;s Yahoo Pipes tutorial&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ms_popfly&quot;&gt;MS Popfly&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.popfly.com/&quot; class=&quot;bare&quot;&gt;http://www.popfly.com/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;설명 : &lt;a href=&quot;http://en.wikipedia.org/wiki/Popfly&quot; class=&quot;bare&quot;&gt;http://en.wikipedia.org/wiki/Popfly&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Silverlight 기술에 기반&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;시연동영상 : &lt;a href=&quot;http://www.youtube.com/watch?v=hkTdJAYb&amp;#8212;&amp;#8203;M&quot;&gt;Three Minut Mashup - Microsoft Popfly&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;ibm_lotus_mashup&quot;&gt;IBM lotus mashup&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ibm.com/lotus/mashups&quot; class=&quot;bare&quot;&gt;http://www.ibm.com/lotus/mashups&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2008년 중반에 공개 예정&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;google_mashup_editor&quot;&gt;Google Mashup Editor&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.googlemashups.com/&quot; class=&quot;bare&quot;&gt;http://www.googlemashups.com/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;설명 : &lt;a href=&quot;http://en.wikipedia.org/wiki/Google_Mashup_Editor&quot; class=&quot;bare&quot;&gt;http://en.wikipedia.org/wiki/Google_Mashup_Editor&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;intel_mash_maker&quot;&gt;Intel Mash maker&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://mashmaker.intel.com/web/&quot; class=&quot;bare&quot;&gt;http://mashmaker.intel.com/web/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;웹브라우저에 확장된 기능을 제공하는 형식&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;시연동영상 : &lt;a href=&quot;http://blip.tv/file/get/Intel_SW-IntelMashMakerOverviewVideo814.wmv&quot; class=&quot;bare&quot;&gt;http://blip.tv/file/get/Intel_SW-IntelMashMakerOverviewVideo814.wmv&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;관련링크&quot;&gt;관련링크&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://uzys.tistory.com/97&quot;&gt;mashup 도구&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Effective Java에 대한 서평에 재미있는 댓글</title>
      <link>https://blog.benelog.net//1847991.html</link>
      <pubDate>Thu, 17 Apr 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1847991.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Effective Java의 Second Edition이 나올 때가 되지 않았나 싶어서 아마존에 가봤더니 아직이군요. 5월28일로 발매가 예정되어 있습니다. (&lt;a href=&quot;http://www.amazon.com/Effective-Java-Second-2nd/dp/0321356683/ref=pd_bbs_sr_1?ie=UTF8&amp;amp;s=books&amp;amp;qid=1208402810&amp;amp;sr=8-1&quot; class=&quot;bare&quot;&gt;http://www.amazon.com/Effective-Java-Second-2nd/dp/0321356683/ref=pd_bbs_sr_1?ie=UTF8&amp;amp;s=books&amp;amp;qid=1208402810&amp;amp;sr=8-1&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;대신 Effective Java의 서평 중에서 재밌는 내용을 발견했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.amazon.com/review/R296QBPY6ZC2LB/ref=cm_cr_pr_cmt?_encoding=UTF8&amp;amp;ASIN=0201310058#wasThisHelpful&quot; class=&quot;bare&quot;&gt;http://www.amazon.com/review/R296QBPY6ZC2LB/ref=cm_cr_pr_cmt?_encoding=UTF8&amp;amp;ASIN=0201310058#wasThisHelpful&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Spend a couple years writing Java code. Then come back, re-read the book, and revisit your review. You will change your mind, or career.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음에 서평쓴 사람이 컴퓨터 과학전공 졸업자로서 35%의 내용정도만 유익했거나 재미있었다고 하니 밑에 답글로 &apos;2년 정도 Java코드를 작성해보고 다시 책을 읽고 다시 서평을 쓴 것을 찾아봐라. 너는 생각을 바꾸게 되거나 진로를 바꾸게 될 것이다.&apos; 라고 적었군요. ^^;&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>마인드맵(mind map) 그리기 도구들</title>
      <link>https://blog.benelog.net//1846356.html</link>
      <pubDate>Wed, 16 Apr 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1846356.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;마인드맵_관련_자료&quot;&gt;마인드맵 관련 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://moai.tistory.com/197&quot;&gt;생각을 확장해주는 마인드 맵(Mind Map)&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://news.naver.com/main/read.nhn?mode=LSD&amp;amp;amp;mid=sec&amp;amp;amp;sid1=101&amp;amp;amp;oid=008&amp;amp;amp;aid=0000858692&quot;&gt;마인드맵핑, 회의 생산성 200% 향상&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;마인드맵_도구와_관련_포스트&quot;&gt;마인드맵 도구와 관련 포스트&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.mindmeister.com/&quot;&gt;MindMeister&lt;/a&gt; : 웹기반 공유 마인드맵.&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://hsgames.com/blog/160&quot;&gt;웹2.0 마인드맵도구 Mindmeister&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://ypshin.com/2690262&quot;&gt;웹상에서 작성하는 무료 MindMap - mindmester.com&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://dirtybit.tistory.com/118&quot;&gt;mindmeister를 소개합니다&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.thoughtex.net/&quot;&gt;Thoughtex&lt;/a&gt; : 웹기반, WPF 기반&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/sjkang73/80038617402&quot;&gt;WPF 기반의 온라인 마인드 맵 도구 입니다.&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.mind42.com/&quot;&gt;Mind42&lt;/a&gt; : 웹기반&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://alones.kr/blog/824&quot;&gt;웹 기반의 멋진 마인드맵 (mindmap) - Mind42 를 써보자&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.conceptdraw.com/&quot;&gt;ConceptDraw&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://boomup.tistory.com/entry/%EB%A7%88%EC%9D%B8%EB%93%9C%EB%A7%B5%EC%9C%BC%EB%A1%9C-%EC%9D%B4%EB%B6%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-ConceptDraw-53&quot;&gt;ConceptDraw MINDMAP 5.3 비주얼 이북 만들기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.thinkwise.co.kr/main/main.asp&quot;&gt;ThinkWise&lt;/a&gt; :&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/nihilbox/140050405578&quot;&gt;MindManager에서 ThinkWise로&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.mindmapper.com/&quot;&gt;MindMapper&lt;/a&gt; : &lt;a href=&quot;http://www.thinkwise.co.kr/main/main.asp&quot;&gt;ThinkWise&lt;/a&gt;의 해외 버전&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.mindjet.com/us/&quot;&gt;MindManager&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://freemind.sourceforge.net/&quot;&gt;freemind&lt;/a&gt; : Java 기반, 오픈소스&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://skcha.tistory.com/10&quot;&gt;마인드 맵을 그리는 FreeMind&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://hanburn.tistory.com/59&quot;&gt;무료 마인드맵(Mind Map) - Freemind&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.smartdraw.com/&quot;&gt;SmartDraw&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.conceptleader.com/&quot;&gt;ConceptLeader&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;자료나 생각을 정리할 때 마인드맵을 활용하고 싶다는 생각은 오래 전부터 해왔지만 쉽게 실현하지는 못했습니다. 제가 마인드맵을 처음 접한 것은 고등학교 때였는데, 몇 번 정도 공책필기 내용을 정리해보다가 그만두고 말았었습니다. 정리하는 내용이 재미가 없어서도 그랬었겠지만, 아무래도 책에서 본 예제처럼 이쁘게 마인드맵이 그려지지 않았던 이유도 컸었던 것 같습니다. 그후로도 몇번 마인드맵과 마주쳤지만 선뜻 작성해볼 의지가 생기지는 않았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위에 소개된  마인드맵 작성을 도와주는 프로그램들이 저 같은 사람에게 도움이 될 것입니다. 여러 도구를 다 써봐도 여전히 손으로 그리는 것이 가장 효율적이라고 생각하시는 분도  계실 것 같습니다만, 저의 경우는 손재주가 없어서 인지 프로그램이 더 편하게 느껴지더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 중에 &lt;a href=&quot;http://www.mindmeister.com/&quot;&gt;MindMeister&lt;/a&gt;, &lt;a href=&quot;http://www.mind42.com/&quot;&gt;Mind42&lt;/a&gt; , &lt;a href=&quot;http://www.thoughtex.net/&quot;&gt;Thoughtex&lt;/a&gt; 는 별도의 프로그램 설치없이 웹에서 마인드맵을 그릴 수 있습니다. 공동작업을 하거나 작업결과를 웹으로 공개할 때 편하게 쓸 수 있어 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 얼마전에 알게된 &lt;a href=&quot;http://www.conceptleader.com/&quot;&gt;ConceptLeader&lt;/a&gt; 는 연결선의 시각적 효과가 돋보입니다.
제가 읽었던 마인드맵을 설명하는 책에서는  선을 뿌리 쪽에는 두껍께, 말단부로 갈 수록 가늘게 그리라고 되어있는데, 다른 프로그램들에서는 이런 효과를 줄 수 없었습니다.  &lt;a href=&quot;http://www.conceptleader.com/&quot;&gt;ConceptLeader&lt;/a&gt;는 홈페이지에서 사용자 등록을 하면 상업적이지 않은 목적의 개인사용자는 무료로 쓸 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://conceptleader.com/conceptleadercokr/01_pro/images/screen_01.jpg&quot; alt=&quot;conceptLeader.JPG&quot; width=&quot;740&quot; height=&quot;672&quot; title=&quot;conceptLeader.JPG&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Java Coding Convention 관련 문서들</title>
      <link>https://blog.benelog.net//1823165.html</link>
      <pubDate>Tue, 1 Apr 2008 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1823165.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;coding_convention_문서&quot;&gt;Coding Convention 문서&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;sun에서_제시한coding_convention&quot;&gt;Sun에서 제시한Coding convention&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/codeconv/index.html&quot; class=&quot;bare&quot;&gt;http://java.sun.com/docs/codeconv/index.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html&quot;&gt;http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/codeconv/html/CodeConventions.doc5.html#16817&quot;&gt;http://java.sun.com/docs/codeconv/html/CodeConventions.doc5.html#16817&lt;/a&gt; : 블럭 내에서 변수선언 위치&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Effective Java (Joshua Bloch저 이해일역)의 7장 프로그래밍 일반 - 항목29 (지역변수의 유효범위를 최소화하라)에서 나온 의견과 다소 어긋남&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;기타&quot;&gt;기타&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://g.oswego.edu/dl/html/javaCodingStd.html&quot; class=&quot;bare&quot;&gt;http://g.oswego.edu/dl/html/javaCodingStd.html&lt;/a&gt; : Doug Lea&amp;#8217;s Java Coding standard&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.chimu.com/publications/javaStandards/part0003.html&quot; class=&quot;bare&quot;&gt;http://www.chimu.com/publications/javaStandards/part0003.html&lt;/a&gt; : Chimy Inc coding standards.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.ambysoft.com/javaCodingStandards.html&quot; class=&quot;bare&quot;&gt;http://www.ambysoft.com/javaCodingStandards.html&lt;/a&gt; : Scott Ambler&amp;#8217;s coding convention&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;한글_자료&quot;&gt;한글 자료&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html&quot; class=&quot;bare&quot;&gt;http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/hyowong/80000631451&quot;&gt;http://blog.naver.com/hyowong/80000631451&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/jeany4u/20003876157&quot; class=&quot;bare&quot;&gt;http://blog.naver.com/jeany4u/20003876157&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;그림&quot;&gt;그림&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/2246773D5186152416&quot; alt=&quot;2246773D5186152416&quot; width=&quot;926&quot; height=&quot;1140&quot; title=&quot;xplor/xp0002f/codingstd.gif&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>shuffle을 이용한 로또생성기</title>
      <link>https://blog.benelog.net//1646013.html</link>
      <pubDate>Fri, 14 Dec 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1646013.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;바로 아래 포스팅에서 간단한 로또생성기 코드를 만들어봤었죠. (&lt;a href=&quot;http://blog.benelog.net/1642193&quot; class=&quot;bare&quot;&gt;http://blog.benelog.net/1642193&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;같은 문제에 대한 dak99님의 풀이(&lt;a href=&quot;http://dak99.egloos.com/3529054)를&quot; class=&quot;bare&quot;&gt;http://dak99.egloos.com/3529054)를&lt;/a&gt; 보고 다시 다른 버전을 만들어봤어요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;shuffle을 이용해도 &lt;a href=&quot;http://java.sun.com/j2se/1.5.0/docs/api/java/util/Collections.html&quot;&gt;Collections&lt;/a&gt;의 메서드들을 불러서 쓰니 코드가 별로 안 복잡해 지는군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class LottoMachine \{
    public static void main(String[] args) \{
        List&amp;lt;Integer&amp;gt; numberCards = new ArrayList&amp;lt;Integer&amp;gt;();
        for(int i=1;i&amp;lt;=45;i++) numberCards.add(i);
        Collections.shuffle(numberCards);
        List&amp;lt;Integer&amp;gt; picked = numberCards.subList(0,5);
        Collections.sort(picked);
        System.out.println(picked);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;n이 클수록 속도차이가 클겁니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>로또생성기 java로 만들어보기</title>
      <link>https://blog.benelog.net//1642193.html</link>
      <pubDate>Wed, 12 Dec 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1642193.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;javaservice.net에서 보고 저도 한번 짜봤어요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( 원글: &lt;a href=&quot;http://www.javaservice.net/%7Ejava/bbs/read.cgi?m=resource&amp;amp;b=qna2&amp;amp;c=r_p&amp;amp;n=1196139164&amp;amp;p=3&amp;amp;s=t#1196139164&quot;&gt;http://www.javaservice.net/~java/bbs/read.cgi?m=resource&amp;amp;b=qna2&amp;amp;c=r_p&amp;amp;n=1196139164&amp;amp;p=3&amp;amp;s=t#1196139164&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;1부터 45까지의 숫자 중에 6개를 뽑는다.6개의 값이 다 달라야 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;java.util.Random를 이용해서 임의의 값을 구한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;출력시 작은 숫자부터 순서대로 출력&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;저의_풀이&quot;&gt;저의 풀이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;간단하게 짠다면 일단 저는 아래와 같이 해보고 싶어요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;
public class LottoMachine1 {
    public static void main(String[] args) {
        SortedSet&amp;lt;Integer&amp;gt; pickedNumbers = new TreeSet&amp;lt;&amp;gt;();
        Random random = new Random();
        while(pickedNumbers.size()&amp;lt; 6) pickedNumbers.add(random.nextInt(45)+1);
        System.out.println(pickedNumbers);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>무엇인가를 만들고 싶다는 꿈</title>
      <link>https://blog.benelog.net//1617365.html</link>
      <pubDate>Thu, 29 Nov 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1617365.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마이크로소프트웨어지 2007년 11월호의 특집기사는 &apos;개발고수 7인의 천기누설, 슈퍼개발자로 가는 길&amp;#8217;이였습니다. 왠지 얄팍해 보이는 제목이 마음에 들지 않았고,  내용을 읽기 전까지는 이제 이 잡지도 소재가 고갈되었나 하는 생각까지도 들었습니다. 그러나 거기에 포함된 델파이 고수 양병규님의 기사는 충격적일 정도로 감명을 주는 내용이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;취재기자의 글 : &lt;a href=&quot;http://www.acornpub.co.kr/blog/158?TSSESSION=6fcc3ea66788b21722c8145a1dedad41&quot;&gt;당신의 가슴 속에 품은 꿈은 무엇인가요?&lt;/a&gt;, &lt;a href=&quot;http://flytgr.tistory.com/193&quot;&gt;꿈꾸는 개발자의 희망스토리&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 기사에서는 양병규님이 PC원격제어 프로그램을 혼자서 2개월만에 개발한 이야기가 나옵니다. 양병규님은  다른 PC원격 제어 프로그램을 처음 보는 순간부터  그런 프로그램을 직접 만들어보고 싶은 마음이 들었고, 그후로 부터 실제 개발을 시작한 시기 사이의 2년동안 틈틈히 아이디어를 생각하고 메모를 했다고 합니다. 화면 캡쳐, 이미지 압축, 소켓 전송 등 프로그램에 필요한 구체적인 것까지 다 미리 생각해 둔 덕분에 실제 개발기간인 2개월동안에는 코딩만 하니 끝이였다고 하네요. 대단하지 않습니까?  아래는 그 기사에서 인용한 내용입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 방법으로 작은 수첩하나를 가슴에 품고 자신의 생각들을 차곡차곡 쌓아가다 보면, 방금 산 복권을 양복 주머니에 넣고 희망찬 모습으로 걸어가는 모습의 복권 포스터 주인공 못지 않게 희망찬 하루하루를 살 수 있을 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/wogud71/20006856915&quot;&gt;델마당 양병규 님의 40대 초보 프로그래머에 대한 답변&lt;/a&gt; 글에 있는 양병규님의 첫 프로그램 이야기도 감동적입니다. 저의 부모님께서도 예전에 어렵게 생활하셨던 분들이라, 어른들의 젊었을 적에 고생한 사연은 어릴 때부터 하도 많이 들어왔기에 왠만한 이야기들은 저는 무덤덤하게 느껴집니다. 하지만 그런 저도 양병규님의 이야기를 읽은 뒤에는 저절로 고개가 숙여집니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 글들을 다 보고나니, 제가 프로그램 개발을 하게 된 이유를 한동안 잊어먹고 있었다는 것을 깨달았습니다. 뭔가를 만들어보고 싶다는 마음, 만드는 과정의 즐거움, 결과물을 볼 때의 뿌듯함, 혹시나 다른 사람에게 도움이 될 수 있을까 하는 기대감.. 그런 감정들이였습니다.  다른 많은 분들도 마찬가지 일 듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런 시작을 잊고 살다 보니 만들어보고 싶은 것이 생각나도 &apos;시간나면 해봐야지&amp;#8217;하는 마음으로 흘려보낸 시간들이 많았습니다. 앞으로는 자주 안 잊어먹도록 여기에도 적어놓습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제 꿈은 아래에 나오신 분들 같은 &apos;발명 할아버지&amp;#8217;가 되는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;이명(99세) 할아버지 :  &lt;a href=&quot;http://news.naver.com/main/read.nhn?mode=LSD&amp;amp;mid=sec&amp;amp;sid1=102&amp;amp;oid=037&amp;amp;aid=0000003035&quot;&gt;나이도 못 막은 열정 ‘98세 발명왕’&lt;/a&gt;  (주간동아)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;최태진(70세) 할아버지 : &lt;a href=&quot;http://www.cbs.co.kr/Nocut/Show.asp?IDX=163494&quot;&gt;밥 먹다가도 작업실 뛰어가&lt;/a&gt; (노컷뉴스)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;정선영(70세) 할아버지 : &lt;a href=&quot;http://news.chosun.com/site/data/html_dir/2007/11/21/2007112100029.html&quot;&gt;“죽기전에 딱 5개만 더 발명하는게 소원” ‘정력팬티’로 100개국 특허 따낸 정선영씨&lt;/a&gt; (조선일보)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;정병일(73세) 할아버지 : &lt;a href=&quot;http://news.naver.com/main/read.nhn?mode=LSD&amp;amp;mid=sec&amp;amp;sid1=102&amp;amp;oid=005&amp;amp;aid=0000197682&quot;&gt;농기계 발명 구슬땀 귀농 70대 부부 화제&lt;/a&gt; (국민일보) , &lt;a href=&quot;http://netv.sbs.co.kr/skin/skin_naver.jsp?uccid=10000181896&quot;&gt;발명 삼매경에 빠진 할아버지&lt;/a&gt; (SBS 생방송 모닝와이드 1818화 3부)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;김병문(65세) 할아버지 : &lt;a href=&quot;http://netv.sbs.co.kr/portal/review.jsp?vod_id=V0000210215&amp;amp;vod_cnt1=02556&amp;amp;vod_cnt2=03&quot;&gt;가위 든 맥가이버&lt;/a&gt;(SBS 출발 모닝와이드 2556회 3부 )&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(간혹 방송이나 인터넷에서  몇 분을 보기는 했는데 찾아보니 이런 할아버지들 꽤 많군요)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2050년쯤 &apos;발명 할아버지&amp;#8217;로 검색했을 때 어느 시골의 마을신문에서 낸 기사로 &apos;집념의 발명 할아버지 정상혁&amp;#8217;과 비슷한 제목이 뜬다면 제가 꿈을 이뤘다고 보셔도 됩니다. 혹시나 그 기사에 &apos;그러나 만든 것 중 쓸모있다고 인정받은 것은 하나도 없음&apos;, &apos;주변에서 계속 뜯어 말리고 있으나 소용없음&apos;,&apos;가족들은 갈수록 헛소리가 심해진다고 걱정이 많음&amp;#8217;과 같은 내용이 포함된다고 해도 말이죠.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Ship it! 성공적인 소프트웨어 개발 프로젝트를 위한 실용 가이드</title>
      <link>https://blog.benelog.net//1546671.html</link>
      <pubDate>Mon, 22 Oct 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1546671.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=2657930&amp;amp;CategoryNumber=001001003005006001&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://image.yes24.com/momo/TopCate57/MidCate09/5683498.jpg&quot; alt=&quot;momo/TopCate57/MidCate09/5683498.jpg&quot; width=&quot;168&quot; height=&quot;220&quot; title=&quot;momo/TopCate57/MidCate09/5683498.jpg&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마이크로소프트웨어지에 &apos;커뮤니티 노트&amp;#8217;라는 칼럼을 연재하고 계신 &lt;a href=&quot;http://kaistizen.net/&quot;&gt;최재훈님&lt;/a&gt;이 번역하신 책입니다. 사실 처음 책의 목차만을 보았을 때는 &apos;실용주의 프로그래머&amp;#8217;와 겹치는 내용이 많을 것 같아서 큰 기대를 하지는 않고 읽기 시작했었습니다. 아마  제가 블로그를 통해서 이 책을 알지 못하고 서점에서 우연찮게 만났다면 책을 안 사고 지나쳤을 법도 합니다. 그런데 다 읽고나니 다음에 프로젝트 시작할 때는 사비로라도 이 책을 사서 프로젝트 팀원들에게 돌리고 싶다는 생각이 들더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책에서는 프로젝트를 성공적으로 이끄는 해결책으로 다음의 방법들을 제시하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;소스코드 버전관리 프로그램을 이용해라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;테스트 자동화를 해라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로젝트 첫날 빌드스크립트를 작성해서 자동화하고, 지속적으로 통합, 빌드, 테스트를 해라.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;일일회의로 개발자들간의 의사소통을 활발히 해라&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;상호 코드 검토를 해라.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아마 이미 식상한 이야기라고 느끼실 분도 많을 것 같네요. 저도 요약한 것만 봤다면 그렇게 느꼈을 것 같습니다. 그러나 이 책의 조언들은 현장감이 넘칩니다. 상세한 행동 방법들과 잘 되어 가고 있는지 확인할 수 있는 검토사항들을 알려주고 있습니다. 부록으로 책에서 말한 지침을 실천하는데 도움이 되는 테스트 프레임웍 같은 도구들의 URL을 모아서 정리해주는 친절함도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;요즘과 같이 지식이 잘 전파되는 환경에서 위의 조언들을 모르고 있는 개발조직은 거의 없을 것입니다.  &apos;외국에서나 통하지 우리나라 현실에 안 맞는 방법이다. &apos; , &apos;실무를 모르고 책상에 앉아서 만든 이야기일 것이다&apos;, &apos;내가 프로젝트를 많이 해봐서 아는데 다 이상적인 내용일 뿐이다.&apos; 등의 생각으로 적용을 안하고 경우도 많습니다. 또는 당장 급하게 돌아가게 있는 프로젝트에서 변화를 주는 것은 위험하다고 판단하기도 하겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저도 그런 경험들을 거쳐왔었습니다.   몇년전에 제가 경험했던 프로젝트에서는  본사차원에서 버전관리 프로그램(PVCS Merant version manager)를 쓰도록 표준이 정해졌었고, 그 프로그램에 대한 설치,  개발자와 고객에게 같이 교육까지도 지원되었습니다. 당시에 저도 버전관리 프로그램이 좋다는 말은 책에서 들어서 알고 있었지만, 막상 쓸려고 하니 괜히 소스관리에 시간만 더 들이게 되는 것이 아닌가 하는 생각이 들더군요. 그리고 PVCS에 check in 한 다음에도 취합, 컴파일, 파일업로드는 한 개발자의 PC에서 이루어 졌기 때문에 실제로 버전관리는 해봤자 백업의 의미밖에 없었습니다. 소스의 최종버전은 결국 개발자들의 각자의 PC에 저장되어 있었고, 버전관리는 프로젝트 기간동안 한 두번 정도 한꺼번에 파일을 올리는 식으로 형식적으로 하고 넘어갔습니다. 파일서버에 각 개발자들이 그날 작업한 것을 폴더 형식을 맞춰서 올리면 한 명이 개인PC로 복사, eclipse로 컴파일, 알ftp로 업로드하는 일을 했는데, 한번에 30~40분은 걸리는 작업을 거의 매일 했었던 것으로 기억합니다.  통합,컴파일, 업로드까지 한꺼번에 해주는 빌드스크립트를 작성하면 좋겠다는 생각도 있었지만, 당장 매일매일 오늘은 프로그램 몇개 짰다고 엑셀파일에 체크를 안 하면 죄인이 되는 것 같은 프로젝트 분위기에 잠깐 다른 일을 할 엄두가 안 나더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음에 이어진 프로젝트는 연속사업이였기 때문에 운영서버에서 돌아가는 소스와 새로 개발되는 소스가 복잡하게 꼬일 위험이 있었습니다. 그 때부터는 버전관리의 필요성을 팀원 모두가 어느정도 인식하기 시작했었습니다.  그래서 빌드 스크립트를 작성하는데 시간을 쓸 수가 있고, 컴파일, 새로 바뀐 파일만 복사, 소스업로드, 서버 리부팅, 로그 남기기의 작업을 한번에 해주는 ant build script를 작성했습니다. 빌드 스크립트를 윈도우 스케줄러에 걸어서 하루에 두 번 돌리고 로그만 아침에 출근해서 한번, 점심 먹고 한번 확인해주고 에러난 것 있으면 개발자에게 통보해 주는 절차를 거쳤습니다.  당시의 제 지식의 한계로  &lt;a href=&quot;http://cruisecontrol.sourceforge.net/&quot;&gt;크루즈컨트롤&lt;/a&gt; 같은 툴을 활용해서 완벽한 CI를 해보는 수준까지 생각 못한 것이 아쉽기는 합니다.  그래도 그 수준이라도 만들고 나니  전에 프로젝트에서 했던 원시적인 작업방식들은 이제 다시는 불편해서 못할 것만 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개발자들의 버전관리 프로그램을 통해서만 소스를 수정한다는 것을 고객이 인식을 하고 나니, 몇몇 고객들은 자신의 PC에 버전관리 프로그램을 설치해 달라고 요구하기 시작했습니다. 그리고 간단한 글자수정 정도는 스스로 하는 고객도 나왔습니다. 비록 &amp;lt;font&amp;gt; 태그로 제가 별로 맘에 안 드는 색깔로 도배를 해서 찜찜해진 페이지도 있었지만, 그래도 고객에 취향에 맞는 결과를 제가 손대지 않고 얻었으니 만족해야겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지금 돌아보면 이슈관리를 위한 Tracker도 적극적으로 사용했어야 하는데 하는 생각이 듭니다. 연속사업의 첫번째 프로젝트에서는 Tracker도 본사에서 교육지원을 나온 인력이 고객과 개발팀 모두에게 사용법을 강의해 줬으나, 거의 활용되지 못했습니다. 두번째 프로젝트에서는 개발팀내에서만 진도관리 정도만을 체크하는 수준에서 Tracker를 사용했었습니다. 그때는 &apos;어차피 고객이 안 쓰니까 우리만 쓰는 용도로 쓸 수 밖에 없다&amp;#8217;라고 생각했었는데, Version Manager처럼 적극적으로 우리가 쓰서 고객을 끌어들일 시도조차 하지 않고서 너무 쉽게 단정을 내렸었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;프로젝트가 끝난 뒤에 특정 방식이 얼마나 생산성을 향상시켰는지 객관적으로 비교하는 것은 어렵습니다.  프로젝트는 정해진 시간에 딱 한번 하는 것이기 때문에 &apos;조건을 통제한 반복실험&amp;#8217;이 불가능하기 때문입니다. 마음이 받아들이지 못하는 방식이라면 설령 그 프로젝트가 무사히 끝났어도 &apos;그것만 없었으면 더 프로젝트를 편하게 했을텐데&amp;#8217;라고 생각할 것이고, 믿고 있는 방식이라면 지연된 프로젝트라도 &apos;그래도 그거 썼으니깐 이정도로 끝낼 수 있었지.&apos;라고 여길 수도 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 새로운 방식의 적용은 사람의 습관을 바꾸는 일입니다. 누구나 몸에 익은 편한 방식이 있고 그 방식으로도 큰 실패는 없을 것이라고 믿고 있습니다. 더 나아진다는 확실한 증거가 없는데도 기존의 방식을 흔쾌히 바꿀 사람은 흔치 않을 것입니다.  그렇게 공감을 얻는 방식을 같이 찾아간다는 것이 쉽지 않은 일임에도 프로젝트에서 그것을 위한 소통의 노력들은 부족할 때가 많습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저의 경험처럼 아무리 위에서 프로세스를 정해주고 지원을 해줘도 그것을 수행할 사람들이 그 필요성에 대해서 공감하지 못한다면 어떤 좋은 도구나 방법론들도 다 형식적인 것에 그치게 되고, 오히려 프로젝트 진행의 발목을 잡는 짐이 되기도 합니다.  어쩌면 가장 좋은 도구나 방법론은 위에서 똑똑한 사람들이 칼같이 정해준 것이 아닌, 팀원 중 다수가 그것이 좋다고 믿음을 가지고 있고, 그 방식의 전도사가 될 준비가 되어있는 것들일 것입니다. 그 믿음을 공유하고 있다면 팀원들은 그것을 증명하기 위해서 더욱 그 방법을 쓰는 작업에 몰입을 하고 정성을 다할 것이고, 공감하지 못한 상태라면 &apos;거봐~ 그거 내가 안 될거라고 했잖아~&apos; 라는 말을 언제라도 하고 싶어서 마음에 품고 다닐 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;몇년 전의 그 정신없던 프로젝트를 마치고 나서 가장 크게 들었던 후회했던 점은 &apos;왜 그때 더 좋은 방법을 찾아볼 노력을 더 안 해봤고, 찾은 것을 알릴려고 애쓰지 않았을까?&apos; 라는 것이였습니다. 빡빡한 일정은 사람의 시야를 좁게 만들어서 당장 오늘,내일을 위한 임시 방편들만을 양산했고, 결국에는 그것이 더 돌아가는 길이라는 것도 깨달을 수도 없었습니다. 프로젝트 초기에 전체 개발팀 회의만 몇번 더 하고 코드리뷰만 몇번 했더라면 그 약간의 시간이 그냥 앉아서 코딩하는 시간의 몇배가 가치가 있었다는 것을 확신하지 못했고, 그렇게 투자하지 못했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책에서 제가 가장 감명 깊게 읽은 내용은 다음 부분이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최고의 아키텍처는 상아탑의 &apos;아키텍트&amp;#8217;에게서 나오지 않습니다. 공동의 노력을 기울여야 최고의 아키텍처가 나옵니다. 전문가 한 명이 독주해서 완성한 아키텍처 문서를 여러분 앞에 던져 놓고 가게 하지 말고, 팀이 함께 일해서 모든 이의 경험을 발휘하고 쌓아나가도록 하세요.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 7월7일, &lt;a href=&quot;http://p-camp.tistory.com/entry/about&quot;&gt;P-camp&lt;/a&gt;라는 행사에 참가했을 때, 저는 &apos;&lt;a href=&quot;http://benelog.springnote.com/pages/349170&quot;&gt;함께하는 개발표준 만들어 가기&lt;/a&gt;&apos;라는 글을 썼던 적이 있습니다. 프로젝트 후에 가장 절실하게 느꼈던 점을 정리했던 것인데, 비슷한 이야기를 이 책에서 보니 당연히 반가웠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;책을 읽다보니 계속 &apos;다음 프로젝트에서는 이런이런 일을 하고, 이런 말을 사람들에게 해야지..&apos; 하는  상상과 기대를 해보게 되었습니다. 힘들고 황폐한 프로젝트라도 조금이나마 불행을 덜어줄 수 있는 일을 제가 할 수 있었으면 하는 꿈이 다시 불어넣어졌습니다. 앞으로의 현실이 어떨지는 몰라도, 이책을 보면 &apos;그래, 이제 제대로 해보는 거야.&apos;라는 말이 이 책의 요약으로 머리속에 떠오를 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;책에 첫장에는 다음과 같은 명언이 인용되고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재의 우리는 반복적으로 하는 행동의 결과이다. 그러므로 탁월함이란 행동이 아니라 습관이다. -아리스토텔레스&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 개발자들이 가질 수 있는 탁월한 습관은 &apos;더 나은 방법이 있다는 것을 믿고, 찾고, 나누는 습관&amp;#8217;이라고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>일본서점 컴퓨터 책 코너 탐방기</title>
      <link>https://blog.benelog.net//1486908.html</link>
      <pubDate>Fri, 21 Sep 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1486908.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 8월4~7일 여름휴가로 일본 동경을 갔다왔습니다. 동경의 번화가를 구경하는데 4일을 다 썼어도 아쉬움이 남을 정도로 구경거리가 많더군요. 그 와중에서도 일본에는 어떤 컴퓨터 책들이 나오고 있을까 하는 호기심을 참지못해 시부야에 있는 북퍼스트와  신주쿠의 기노쿠니아 서점을 가 봤었습니다. 사실 일본어는 &apos;다꾸앙&apos;,&apos;와리바시&apos;,&apos;덴뿌라&apos; 수준의 단어 밖에 모르고, 글자는 전혀 읽을 줄 모르기 때문에 책표지만 감상할 수 밖에 없었습니다. 그래도 전체적인 기류는 명확히 와 닿았기에 인상적인 경험이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/kinoku.jpg&quot; alt=&quot;kinoku.jpg&quot; width=&quot;320&quot; height=&quot;240&quot; title=&quot;01kinoku.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/book1st.jpg&quot; alt=&quot;book1st.jpg&quot; title=&quot;book1st.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;역시 웹2.0, SNS(Social Network Service), SEM(Search Engine Marketing), SEO(Search Engine Optimization, 검색엔진 최적화) 에 관련된 책들이 컴퓨터 서적코너의 앞 쪽을 차지하고 있었습니다. 인터넷 비지니스에 대한 열기는 일본에서도 뜨겁게 느껴집니다.
SEM/SEO는 아직 국내에서는 책 제목으로 붙일 정도로 보편화된 용어는 아닌 것 같은데 일본에서는 어느정도 알려졌나 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/web2-0-magazine.jpg&quot; alt=&quot;web2-0-magazine.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/web2-0_1.jpg&quot; alt=&quot;web2-0_1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/web2-0_2.jpg&quot; alt=&quot;web2-0_2.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/web2-0_3.jpg&quot; alt=&quot;web2-0_3.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;구글,  Yahoo, Myspace등 인터넷 서비스에 대한 책들도 많이 보였습니다. 서비스 사용자를 위한 이용법에 대한 내용도 있었지만, 개발자를 위한 Open API관련 서적이 눈에 띄게 많았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/google.jpg&quot; alt=&quot;google.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/google-api.jpg&quot; alt=&quot;google-api.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/google-yahoo.jpg&quot; alt=&quot;google-yahoo.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/google_1.jpg&quot; alt=&quot;google_1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Goole earth에 관한 책들이 몇권보이는 마지막 사진은 컴퓨터 코너가 아닌 기노쿠니아 서점의 지학(땅 지 ,배울 학) 코너에서 찍은 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Second life에 대한 책도 진열장의 잘 보이는 곳을 차지하고 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/214615&quot; alt=&quot;secondlife.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/214610&quot; alt=&quot;secondLife3.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/214609&quot; alt=&quot;secondLife1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;마지막 사진은 기노쿠니아 원서 코너에서 찍은 것인데, 그 서점에는 컴퓨터 원서가 몇권 없었음데도 second life와 my space에 대한 책이 보였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java관련 책들을 찾아봤는데 반가운 책들을 여러 권 발견했습니다. Effective Java, Expert one and one J2EE developement, Core J2EE pattern.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/java_1.jpg&quot; alt=&quot;java_1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/java_2.jpg&quot; alt=&quot;java_2.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/java_3.jpg&quot; alt=&quot;java_3.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eclipse 관련 서적들도 비중있게 자리가 배정되어 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/eclipse_1.jpg&quot; alt=&quot;eclipse_1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/eclipse_2.jpg&quot; alt=&quot;eclipse_2.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;오픈소스 프레임웍에 관한 책들도 찾아보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/opensource-fw.jpg&quot; alt=&quot;opensource-fw.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/spring2-0.jpg&quot; alt=&quot;spring2-0.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 국내에서는 보기 힘든 책의 번역판도 보였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.amazon.com/Inside-JavaOS-TM-Operating-System/dp/0201183935/ref=sr_1_1/103-8441353-8681409?ie=UTF8&amp;amp;amp;amp;amp;amp;amp;amp;amp;s=books&amp;amp;amp;amp;amp;amp;amp;amp;amp;qid=1190370137&amp;amp;amp;amp;amp;amp;amp;amp;amp;sr=8-1&quot;&gt;Inside JavaOS&lt;/a&gt;라는 책인데 돌아와서 찾아보니 yes24에서는 외국서적으로도 검색이 안 되는 책이더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/java-os.jpg&quot; alt=&quot;java-os.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;함수형 언어 Haskell에 대한 책도 발견했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/haskel.jpg&quot; alt=&quot;haskel.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;두 서점을 통들어 가장 강렬한 느낌을 주었던 책장은 &lt;a href=&quot;http://www.wingbus.com/asia/japan/tokyo/shibuya/book_first/&quot;&gt;북퍼스트&lt;/a&gt;에서 본 Oreli 원서를 모아둔 곳이였습니다. 잘 정리된 모습이 깔끔해 보이네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/oreli_1.jpg&quot; alt=&quot;oreli_1.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 바로 옆에는 Oreli 번역서를 모아둔 책장이 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/japanese-book/oreli_2.jpg&quot; alt=&quot;oreli_2.jpg&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ruby를  일본 사람이 만들었으니 Ruby관련 서적이 한 칸은 차지하고 있지 않을까 하는 상상을 했었으나, 그렇지는 않았습니다. 국내와 비슷해보이는 몇권 정도였던것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;더 구경을 하고 하고 싶었으나,불만 가득한 아내 때문에 적당히 보고 나올 수 밖에 없었습니다. 여기까지 와서 이런 데를 오는 것이 이해가 안 간다고 하더군요. 아내가 옷가게 구경할 때 저는 불평없이 있었다는 사실이 별로 고려되지 않은 듯합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;서점에 가기 전에도 일본이 우리나라보다 인구가 많아서 시장이 넓으니 국내보다 풍부한 책들을 구경할 수 있을 것이라고 예상을 했었습니다.
둘러보니 역시 다양한 책들을 많이 볼 수 있었습니다
.
&lt;a href=&quot;http://www.amazon.com/Inside-JavaOS-TM-Operating-System/dp/0201183935/ref=sr_1_1/103-8441353-8681409?ie=UTF8&amp;amp;amp;amp;amp;amp;amp;amp;amp;s=books&amp;amp;amp;amp;amp;amp;amp;amp;amp;qid=1190370137&amp;amp;amp;amp;amp;amp;amp;amp;amp;sr=8-1&quot;&gt;Inside JavaOS&lt;/a&gt; 같은 책은 소수만이 관심을 가질 것으로 보이고 , 국내에서 이런 책을 읽고 싶은 사람은 아마  아마존에서 직접 주문해서 읽을 것입니다. 이런 전문적인 책들이 일본에 번역판이 나왔다는 사실은 크게 부럽지는 않습니다. 그러나 Open API에 대한 책에 대해서는 아쉬움이 남습니다. 이런 책도 역시 적극적인 의지를 가진 사람은 인터넷으로 자료를 찾거나 원서를 사 보면서 공부할 수 있겠죠. 하지만 영어로 읽는 것이 속도가 늦거나 약간이라도 저렴한 번역서를 사고 싶어하는 사람들을 끌어드릴 수 있다면 더욱 그 분야가 활성화 될 수 있을 것입니다. 즉  대중화하는 것이 의미가 있는 분야의 책은 수요를 보다 도전적으로 창출해야 한다고 생각합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;계속 open API를 예로 들면,
&apos;막연하게 뭔가 open api로 프로그램을 만들고 싶다는 사람이 있다.
그런데 그는 바쁜 업무를 핑계로 적극적으로 관련 자료를 찾아보지 못하고 있다.
우연찮게 그는 서점을 갔다가 Open API에 대한 책을 발견하고 사게 된다.
그는 출퇴근 길이나 집에서 뒹구는 시간에 그 책을 읽고 더욱 흥미를 느끼고, 결국에는 매쉬업 서비스를 만들어서 공개하게 된다.&apos; 와 같은 일도 생길 수 있지 않겠습니까?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음, 네이버, 스프링노트 같은 국내 서비스의 open API에 대한 입문서가 있다면 좋겠다는 생각도 듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;아뭏든 읽지도 못하는 책을 신나게 구경하게 온, 특이한 경험이였습니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>JavaScript, JSUnit으로 풀어본 뉴질랜드 화폐 문제</title>
      <link>https://blog.benelog.net//1483100.html</link>
      <pubDate>Wed, 19 Sep 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1483100.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://yaldex.com/JSFactory_Pro.htm&quot;&gt;1st JavaScript editor&lt;/a&gt;의 공짜 사용기한이 얼마 남지 않아서 JavaScript로 빨리 뭐라도 만들어봐야 겠다는 생각이 들었습니다. 프로젝트 업무 중에는 그런 일이 보이지 않아서 회사의 다른 팀원들이 하고 있는 스터디 모임에서 나왔던 &lt;a href=&quot;http://blog.naver.com/i1j1jsy/70011900056&quot; class=&quot;bare&quot;&gt;http://blog.naver.com/i1j1jsy/70011900056&lt;/a&gt;뉴질랜드 화폐문제를 풀어보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원래 제가 문제를 봤던 URL : &lt;a href=&quot;http://rdp.ahamoment.org/wiki/Dollars&quot; class=&quot;bare&quot;&gt;http://rdp.ahamoment.org/wiki/Dollars&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;요약&quot;&gt;요약&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;뉴질랜드 화폐는 $100, $50, $20, $10, $5 지폐와   $2, $1, 50센트, 20센트, 10센트, 5센트  동전으로 이루어져 있다. 주여진 합계액에 대해서 몇 가지의 방법으로 돈을 구성할 수 있는지 세는 프로그램을 작성해라.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;예) 20센트는 4가지 방법으로 만들수 있다. : 1개의 20센트, 2개의 10센트, 10센트+ 2개의 5센트, 4개의 5센트&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;원래 문제는 파일에서 입력값을 받게 되어 있지만, JavaScript로 풀다보니 그 부분은 생략했습니다. 직접 풀어보실 분들은 풀이를 나중에 보세요~&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&amp;lt;script language=&quot;JavaScript&quot; type=&quot;text/JavaScript&quot; src=&quot;app/jsUnitCore.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script language=&quot;JavaScript&quot; type=&quot;text/JavaScript&quot;&amp;gt;

    var unitList = [100, 50, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05];


    function countWaysOfAmount(totalMoneyAmount, unitIndex, ways)\{
        if (unitIndex == null) unitIndex = 0;
        if (ways == null) ways = 0;

        var unit = unitList[unitIndex];
        var moneyLeft = totalMoneyAmount;

        do\{
          if (moneyLeft == 0) ways++;
          else if (unitIndex &amp;lt; unitList.length)
            ways = countWaysOfAmount(moneyLeft,unitIndex+1,ways);
          moneyLeft =  Math.round( (moneyLeft - unit) *100) /100;
        } while(moneyLeft &amp;gt;= 0)
        return ways;
    }

    function testDollars()\{
        assertEquals(&quot;0.05&quot;, countWaysOfAmount(0.05), 1);
        assertEquals(&quot;0.1&quot;,  countWaysOfAmount(0.1 ), 2);
        assertEquals(&quot;0.15&quot;, countWaysOfAmount(0.15), 2);
        assertEquals(&quot;0.2&quot;,  countWaysOfAmount(0.2 ), 4);
        assertEquals(&quot;2&quot;,  countWaysOfAmount(2), 293);
    }

    alert(countWaysOfAmount(2));
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코드를 더 다듬을 여지가 있는 것 같지만 일단 올려봅니다&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;moneyLeft =  Math.round( (moneyLeft - unit) *100) /100;&lt;/code&gt; 부분은 다른 언어들과 마찬가지로 JavaScript에서도 소숫점 계산이 정확하지 않아서 붙인 부분입니다. 이걸 안해주니 5가 나와야하는 값이 4.999999  같이 나오더군요. 이 문제만 아니면 moneyLeft -= unit 으로 간단하게 쓸수 있는데 말이죠,
Java로 이걸 짠다면 BigDecimal을 쓰거나 별도의 클래스를 정의해야겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;JSUnit 없이 로컬에서 수정해 보실 분은 `&amp;lt;script language=&quot;JavaScript&quot; type=&quot;text/JavaScript&quot; src=&quot;app/jsUnitCore.js&quot;&amp;gt;&amp;lt;/script&amp;gt; 부분과 테스트 메서드만 지우고 돌려 보시면 되겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.naver.com/i1j1jsy/70011900056&quot;&gt; Internet Explorer Developer Toolbar&lt;/a&gt;, &lt;a href=&quot;http://yaldex.com/JSFactory_Pro.htm&quot;&gt;1st JavaScript editor&lt;/a&gt;, &lt;a href=&quot;http://www.jsunit.net/&quot;&gt;JSUnit&lt;/a&gt;, &lt;a href=&quot;https://addons.mozilla.org/ko/firefox/addon/1843&quot;&gt;FireBug&lt;/a&gt; 를 한꺼번에 써서 코딩하니 이클립스가 부럽지 않았습니다 ^^; 그리고 jsunit에서 debug(&quot;로그&quot;); 로 찍어보는 것도 상당히 유용했구요. 1st JavaScript edtior나 FireBug가 모두 디버거 기능이 훌륭하지만 반복문의 값을 추적하는 것은 break point 거는 것보다는 로그로 쫙 찍어보는 것이 편하더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>확장된 나선형 배열 출력 문제(Spiral Array)</title>
      <link>https://blog.benelog.net//1483053.html</link>
      <pubDate>Wed, 19 Sep 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1483053.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 2월에 회사 워크샵에서 풀었던 나선형 배열 문제에 대한 글을 올린 적이 있었지요. ( &lt;a href=&quot;http://blog.benelog.net/901106&quot; class=&quot;bare&quot;&gt;http://blog.benelog.net/901106&lt;/a&gt; )&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;알고보니 xper.org에도 이 문제가 올라와 있더군요 (&lt;a href=&quot;http://xper.org/wiki/seminar/SpiralArray&quot; class=&quot;bare&quot;&gt;http://xper.org/wiki/seminar/SpiralArray&lt;/a&gt;). 거기에 가면 다른 분들의 풀이도 많이 볼 수 있습니다.  저는 이 문제를 실컷 가지고 논 후에 더 이상 손 댈 것이 없다고 느껴질때 다른 분들의 풀이를 꼼꼼히 볼 생각입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우연찮게 이에 대한 확장된 문제를 다른 블로그에서 보게 되었습니다. (&lt;a href=&quot;http://blog.naver.com/itioma?Redirect=Log&amp;amp;logNo=40040118386&quot; class=&quot;bare&quot;&gt;http://blog.naver.com/itioma?Redirect=Log&amp;amp;logNo=40040118386&lt;/a&gt; ). 그 포스트에서 본 문제에는 아래의 조건들이 더 추가되어 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;작성된 코드에서 한 글자만 수정해서 &apos;숫자는 1씩 늘어난다&amp;#8217;를 &apos;숫자는 2씩 늘어난다&amp;#8217;로 바꾸시오.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드에서 한 문장만 수정해서 시작점 좌표(x,y)를 마음대로 바꿀 수 있도록 만드시오&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드에서 한 문장만 수정해서 시작할 때의 진행방향을 (위/아래/왼쪽/오른쪽 중에서 ) 바꿀 수 있도록 만드시오&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;배열의 크기를 마음대로 정할 수 있도록 만드시오.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드에서 한 문장만 수정해서 &apos;시계방향으로 90도&amp;#8217;를 &apos;반시계 반향으로 90&amp;#8217;도로 바꾸시오&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드에서 swich 문이나 else 문을 이용한 3단계 이상의  조건분기가 있다면 모두 제거하시오.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드에서 한 글자만 수정해서 &apos;한칸씩 진행하며&amp;#8217;를 두칸씩 건너뛰며&amp;#8217;로 바꾸시오.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1~7에서 수정되었던 문장/값/변수들이모두 하나의 함수 또는 블럭안에 존재하도록 고치시오.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고   숫자는 1에서 시작하고 , 채워지지 않은 부분이 있다면 0으로 표시된다는 점이 제가 처음에 풀었던 문제와 다르네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;확장된 조건을 다 수용할 수 있는  만족시키는 코드를 다시 한번 만들어보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;소스코드&quot;&gt;소스코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class MatrixMain {
    public static void main(String[] args) {
       System.out.println(&quot;0.원래의 matrix&quot;);
       ExtMatrix mp = new ExtMatrix(6,6);
       mp.print();

       System.out.println(&quot;1.숫자가 2씩 늘어나게&quot;);
       mp.setIncrement(2);
       mp.print();

       System.out.println(&quot;2.시작점을 1,1로&quot;);
       mp.setStartPoint(1, 1);
       mp.print();

       System.out.println(&quot;3.시작방향을 아래부터&quot;);
       mp.setStartDirection(ExtMatrix.DOWN);
       mp.print();

       System.out.println(&quot;4.배열크기 바꾸는건 생성할때 파라미터 바꾸면 됨.&quot;);
       System.out.println();
       System.out.println(&quot;5.반시계 반향으로 회전&quot;);
       mp.setReverseClockWiseTurn();
       mp.print();

       System.out.println(&quot;6.그런 조건 분기 없음.&quot;);
       System.out.println();

       System.out.println(&quot;7.한칸씩이 아닌 두칸씩 건너뛰며.&quot;);
       mp.setMoveStep(2);
       mp.print();
    }
}

public class ExtMatrix {
    public static final int RIGHT = 0;
    public static final int DOWN = 1;
    public static final int LEFT = 2;
    public static final int UP  = 3;

    private static final int INIT_VALUE = 0;
    private static final int CLOCK_WISE_TURN = 1;
    private static final int REVERSE_CLOCK_WISE_TURN = -1;

    private int m = 0;
    private int n = 0;
    private int startX = 0;
    private int startY = 0;
    private int[][] matrix;
    private int startDirection = RIGHT;
    private int increment = 1;
    private int moveStep = 1;

    private int turnDirection = CLOCK_WISE_TURN;


    public ExtMatrix(int m, int n){
        this.m = m;
        this.n = n;
        matrix = new int[m][n];
    }

    public void setIncrement(int increment){
        this.increment = increment;
    }

    public void setStartPoint(int startX, int startY){
        this.startX = startX;
        this.startY = startY;
    }

    public void setClockWiseTurn(){
        turnDirection = CLOCK_WISE_TURN;

    }

    public void setReverseClockWiseTurn(){
        turnDirection = REVERSE_CLOCK_WISE_TURN;
    }

    public void setMoveStep(int moveStep){
        this.moveStep = moveStep;
    }

    public void setStartDirection(int startDirection){
        this.startDirection = startDirection;
    }

    public void print(){
        locateNumbers();
        for (int i=0;i&amp;lt; m ;i++){
            for (int j=0;j&amp;lt;n;j++) System.out.print(matrix[i][j] + &quot;\t&quot;);
            System.out.println();
       }
       System.out.println();
    }

    private void locateNumbers() \{
       init();
       int[] startPosition = new int[]\{startX,startY};
       matrix[startX][startY]= 1;
       int direction = startDirection;
       while(move(startPosition ,direction)) direction= getNextDirection(direction);
    }

    private void init(){
        for (int i=0;i&amp;lt; m ;i++){
            for (int j=0;j&amp;lt;n;j++) matrix[i][j] = INIT_VALUE;
        }
    }

    private boolean move(int[] position, int direction){
        int nowNumber = matrix[position[0]][position[1]];
        boolean moved = false;
        int[] nextPosition =getNextPosition(position,direction);
        while(isMovable(nextPosition[0],nextPosition[1])){
            moved = true;
            nowNumber+= increment;
            position[0] = nextPosition[0];
            position[1] = nextPosition[1];
            matrix[position[0]][position[1]]= nowNumber;
            nextPosition = getNextPosition(position,direction);
        }
        return moved;
    }

    private int[] getNextPosition(int[] position,int direction){
        int x = position[0];
        int y = position[1];
        if (direction == RIGHT) y+= moveStep;
        else if (direction == DOWN) x+= moveStep;
        else if (direction == LEFT) y-= moveStep;
        else if (direction == UP)  x-= moveStep;
        return new int[]\{x,y};
    }

    private int getNextDirection(int direction){
        direction+= turnDirection;
        if (direction&amp;lt;0) direction+=4;
        direction = direction %4;
        return direction;
    }

    private boolean isMovable(int x, int y){
        if (x&amp;gt;=m) return false;
        if (y&amp;gt;=n) return false;
        if (x&amp;lt;0) return false;
        if (y&amp;lt;0) return false;
        if (matrix[x][y]!= INIT_VALUE) return false;
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;방향을 int로 나타내는 것이 처음에는 좋은 아이디어 라고 생각했는데  &lt;code&gt;mp.setStartDirection(ExtMatrix.DOWN);&lt;/code&gt; 부분을 보니 타입안전열거형이나 enum을 도입해야지 좀더 코드가 이뻐질 것 같네요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Sybase 날짜형(datetime)의 문자열 변환시 style number</title>
      <link>https://blog.benelog.net//1478801.html</link>
      <pubDate>Mon, 17 Sep 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1478801.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;sybase에서 select convert(varchar,getdate(),112)로 찍어보면 오늘 날짜가 20070831 형식으로 나오죠. 이런 형식들이 정리된 자료를 한 번 만들어 보았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 쓰는 sybase 버전은 12.5.3입니다. (select @@version)으로 확인할 수 있죠)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;select convert(varchar,날짜데이터, &lt;strong&gt;&lt;em&gt;convertType&lt;/em&gt;&lt;/strong&gt;)`&lt;/code&gt; 형식으로 쓰고 &lt;em&gt;*convertType *&lt;/em&gt; 위치에 숫자가  들어갈 때 옆에 적힌 형식대로 나온다고 보시면 됩니다. 예시로 옆에 찍힌 날짜는 2007년 8월27일입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;style_number&quot;&gt;Style number&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;0 = Aug 27 2007  5:28PM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1 = 08/27/07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2 = 07.08.27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3 = 27/08/07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;4 = 27.08.07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;5 = 27-08-07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;6 = 27 Aug 07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;7 = Aug 27, 07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;8 = 17:23:35&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;9 = Aug 27 2007  5:28:08:563PM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;10 = 08-27-07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;11 = 07/08/27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;12 = 070827&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;13 = 07/27/08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;14 = 08/07/27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;15 = 27/07/08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;16 = Aug 23 2007 17:28:08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;18 = 15:17:08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;19 = 5:11:39:086PM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;20 = 17:12:30:633&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;21 = 07/08/27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;22 = 07/08/27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;100 = Aug 27 2007  5:28PM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;101 = 08/27/2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;102 = 2007.08.07&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;103 = 27/08/2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;104 = 27.08.2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;105 = 27-08-2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;106 = 27 Aug 2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;107 = Aug 27, 2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;108 = 17:28:08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;109 = Aug 27 2007 5:28:08:563PM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;110 = 08-27-2007&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;111 = 2007/08/27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;112 = 20070827&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;113 = 2007/27/08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;114 = 08/2007/27&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;115 = 27/2007/08&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;116 = Aug 23 2007 17:28:08&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;응용&quot;&gt;응용&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;select convert(char,GETDATE(),112)&lt;/code&gt; : 오늘날짜를 YYYYMMDD로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;select convert(char(8), DATEADD(DD,-1,getdate()) ,112)&lt;/code&gt; : 현재 날짜 하루전을 yymmdd형식으로 출력&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;select convert(char(8), DATEADD(DD,-1,&apos;20070827&apos;) ,112)&lt;/code&gt; : 2007년 8월 27일 전날을 출력. string &amp;#8594; datetime은 convert라는 함수를 사용하지 않고 내부적(implicit)으로 자동으로 변경됩니다&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;select str_replace( convert(varchar,getdate(),20),&apos;:&apos;,null)&lt;/code&gt; : 현재 분일초,밀리세컨드까지: 152515853&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;select convert(varchar,GETDATE(),112) || str_replace( convert(varchar,getdate(),20),&apos;:&apos;,null)&lt;/code&gt; : 현재 연월일시분초밀리세컨드를 다 붙여서&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>CSS 선택자 정리</title>
      <link>https://blog.benelog.net//1456715.html</link>
      <pubDate>Thu, 6 Sep 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1456715.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주로 Head first HTML/CSS에 있는 내용을 정리했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;여러개의_element_선택&quot;&gt;여러개의 element 선택&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;h1, h2 { }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;class_선택자&quot;&gt;class 선택자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;p.greentea { } /* class가 greentea인 &amp;lt;p/&amp;gt; 선택 */

.greentea { } /* class가 greentea인 모든 태그 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;id_선택자&quot;&gt;id 선택자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;#footer {} /* id가 footer인 모든 태그 */

p#footer {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;자식child_element_선택&quot;&gt;자식(Child) element 선택&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;div h2 {color:red} /* &amp;lt;div/&amp;gt; 하위에 있는&amp;lt;h2/&amp;gt; 선택  */

.detail p {}   /* class가 &quot;detail&quot;로 지정되어 있는 하위의 &amp;lt;p/&amp;gt; 선택 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;상태에_따른_선택자&quot;&gt;상태에 따른 선택자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;a:visited\{}

a:link \{}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;상태는 active, hover, link, visited, first-child 등&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;의사psdudo_element_선택자&quot;&gt;의사(Psdudo) element 선택자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;p:first-letter {}

p:first-line {}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;속성attribute_선택자&quot;&gt;속성(Attribute) 선택자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;img[width] {border:black thin solid;} /* width 속성을 가진 모든 이미지 선택 */

img[height=&quot;300&quot;] {border:red thin solid; } /* 값이 300인 height 속성을 가진 모든 이미지 선택 */

image[alt~=&quot;flowers&quot;] {border:red thin solid; }  /* &quot;flowers&quot;라는 단어를 포함하는 alt 속성을 가진 모든 이미지 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;형제sibling_element_선택자&quot;&gt;형제(Sibling) element 선택자&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;h1+p {}  /* &amp;lt;h1&amp;gt; 다음에 오는 &amp;lt;p&amp;gt; 선택 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;선택자_결합&quot;&gt;선택자 결합&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;div#greentea &amp;gt; blockquote {}  /* &amp;lt;blockquote/&amp;gt;의 부모가 되어야하는 &quot;greentea&quot; id를 가진 &amp;lt;div/&amp;gt; 자손 선택자 */

div#greentea &amp;gt; blockquote p {} /* &amp;lt;blockquote/&amp;gt;의 자손이자 &quot;greentea&quot; id를 가진 &amp;lt;div/&amp;gt;의 자손인 &amp;lt;p/&amp;gt; 선택 */

div#greentea &amp;gt; blockquote p:first-line /* 그 &amp;lt;p/&amp;gt;의 첫 줄 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>java의 List와 반복문(loop), 그리고 변수 선언 위치에 대해서</title>
      <link>https://blog.benelog.net//1382604.html</link>
      <pubDate>Fri, 3 Aug 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1382604.html</guid>
      	<description>
	&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;수정이력&quot;&gt;수정이력&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;20190409&quot;&gt;2019/04/09&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Github로 글 이전하면서 이미지, 첨부파일 내용 복원&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;20070813&quot;&gt;2007/08/13&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&apos;들어가며&apos;, &apos;마치며&apos; 대폭 수정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;오타 수정  : 여러개의 메서드에서 같이 써는 코드였었습니다.=&amp;#8658; 여러개의 메서드에서 같이 쓰는 코드였었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;20070809&quot;&gt;2007/08/09&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;첫 단락 소제목 변경 ( &quot;ArrayList나 Vector에는 iterator가 성능이 더 안 좋다.&quot;에서 )&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;20070808&quot;&gt;2007/08/08&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;소제목을 h3태그로&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iterator를 쓸때 고려할 점 설명에 내용 보강&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;반복될 필요가 없는 코드의 예 설명에서 오타수정, 문장 다듬기&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;바이트 코드설명 부분의 한 문장 추가, 문장 다듬기&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;들어가며&quot;&gt;들어가며&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 요즘 유지보수 프로젝트에서 일하고 있습니다. 많은 시간을 코드를 다듬는데 쓰고 있고  다른 개발자들의 코드를 많이 볼 수 있는 기회가 되고 있습니다.  그러다 보니 List의 크기만큼 반복문을 돌리는 코드에서 많은 개발자들은  루프 블럭 안에서만 쓰는 변수의 선언을 밖에다 하고 있다는 것을 발견하게 되었습니다.  이에 대해서 개발기간에 대화를 해볼 기회가 없었다는 것이 아쉽게 느껴지더군요. 그래서 그 내용과 함께 list와 loop에 관한 몇가지 이야기들을 같이 묶어서 글로 정리해 보게 되었습니다. 첫번째 소제목과 두번째 소제목 아래의 내용은 성능을 약간이라도 더 개선하고 싶을 때 도움이 될 정보들이고, 세번째 소제목에서 위에서 말한 많은 개발자들의 습관이 실제로는 성능에는 아무 영향이 없음을 설명할려고 했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;arraylist와_vector는_randomaccess_inteface를_구현하고_있다&quot;&gt;ArrayList와 Vector는 RandomAccess inteface를 구현하고 있다.&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;List의 크기만큼 반복문을 도는 방법에는 크게 두 가지 방법이 많이 쓰이고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html&quot;&gt;java.util.List&lt;/a&gt;의 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#size%28%29&quot;&gt;size()&lt;/a&gt;로 크기를 구해서 그 갯수만큼 반복문을 돌아서 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#get%28int%29&quot;&gt;get(int index)&lt;/a&gt;로 List안에 있는 객체를 가지고 온다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://http://java.sun.com/j2shttp://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#iterator%28%29&quot;&gt;iterator()&lt;/a&gt; 로 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/Iterator.html&quot;&gt;java.util.Iterator&lt;/a&gt; 객체를 얻은 후 이 객체의 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/Iterator.html#hasNext%28%29&quot;&gt;hasNext()&lt;/a&gt;가 true인 동안 반복문을 돌아서 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/Iterator.html#next%28%29&quot;&gt;next()&lt;/a&gt;로 List안의 객체를 가지고 온다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그런데 그 객체가 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/ArrayList.html&quot;&gt;java.util.ArrayList&lt;/a&gt;나 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/Vector.html&quot;&gt;java.util.Vector&lt;/a&gt;가 확실할 때는 1번의 방법이 더 빠르다고 합니다. 그 이유는 API문서를 보시면 확인할 수 있듯이 ArryList와 Vector는 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/RandomAccess.html&quot;&gt;java.util.RandomAccess&lt;/a&gt; interface를 구현하고 있기 때문입니다. RandomAccess의 API문서에는 아래와 같이 적혀있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Marker interface used by List implementations to indicate that they support fast (generally constant time) random access.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;List의 구현체가 빠른 임의접근을 지원한다는 것을 나타내는 표시 interface라는 말입니다. 선언되어 있는 메서드도 아무것도 없지요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;API문서에는 이렇게 적혀있지만 성능이 항상 우선순위의 가치는 아니므로 RandomAccess List에는 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#get%28int%29&quot;&gt;get(int index)&lt;/a&gt;을 써야 한다는 법칙을 만들 수는 없을 것입니다. 가령 List와 다른 Collection 객체, 배열 등을 한꺼번에 묶어서 처리할 때는 iterator pattern이 유용합니다. (참고자료5: Head first design pattern).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;하지만 성능이 중요한 코드를 짜는 경우에는  객체를 생성하는 부분에서 ArrayList나 Vector를 생성한다는 정보를 확실히 가지고 있고, 그것이 LinkedList같은 다른 객체로 변경될 가능성이 없다면 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/RandomAccess.html&quot;&gt;java.util.RandomAccess&lt;/a&gt; interface의 명세는 무시할 수만은 없을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;List 처리  시에 성능을 극대화 시키기 위해서 RandomAccess interface를 구현했는지 검사해서  &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#get%28int%29&quot;&gt;get(int index)&lt;/a&gt;를 쓸지 &lt;a href=&quot;http://http//java.sun.com/j2shttp://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#iterator%28%29&quot;&gt;iterator()&lt;/a&gt;를 사용할지 결정하는 코드도 있기는 합니다. (참고자료2의 자바퍼포먼스 튜닝 중).  보통 그렇게까지 성능을 쥐어짤 경우는 흔치 않겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;list의_크기를_반복해서_구할_필요가_없다&quot;&gt;List의 크기를 반복해서 구할 필요가 없다.&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위에서 1번으로 제시된 것처럼 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#get%28int%29&quot;&gt;get(int index)&lt;/a&gt; 를 쓰는 방법을 쓸 때도 아래와 같은 코드를 많이 보게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;for( int i = 0; i &amp;lt; list.size(); i++)\{

//일하기

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;흔하게 보는 코드죠? 그런데 위 코드에도 굳이 필요없는 성능의 손실이 있습니다. 바로 list의 크기를 구하는 &lt;a href=&quot;http://java.sun.com/j2se/1.4.2/docs/api/java/util/List.html#size%28%29&quot;&gt;size()&lt;/a&gt; 메서드가 매번 반복해서 호출된다는 것입니다. 반복문 내에서 list의 크기가 변하는 경우가 아니라면 for문의 초기화 때 한번으로 충분합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;for(int i = 0, n = list.size(); i &amp;lt; n; i++)\{

//일하기

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;n을 for문 앞에서 선언하는 방법도 있지만, for block 밖에서 n이 필요한 경우가 아니라면 n이 for의 초기화 부분에 선언되고 할당되는 것이 좋습니다. 변수의 유효범위가 최소화되기 때문이죠.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;loop안에서만_사용하는_변수의_선언을_loop밖에_해야_할까&quot;&gt;Loop안에서만 사용하는 변수의 선언을 loop밖에 해야 할까?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코드의 중복을 없애라거나 적절한 API를 쓴다거나 등의 바람직한 코드를 위한 지침에는 항상 다음과 같은 이유들이 붙어다닙니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드의 가독성이 좋아진다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;유지보수가 편해진다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;오류의 발생 가능성이 줄어든다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지역변수의 유효범위를 최소화하라는 지침에도 위의 이유들이 역시나  인용되고 있지요. (참고자료1 Effective Java, 참고자료 3 Code completed 2nd Edition). 변수의 선언과 초기화, 사용 사이에 있는 코드가 많을 수록  취약성 있는 코드가 들어갈 여지는 커집니다. 이른 시점부터 쓰이지도 않는 객체가 초기화되어서 메모리를 낭비하고 있을 가능성도 있고, 선언되고 한번도 쓰이지 않는 변수들도 눈에 잘 들어오지 않게 됩니다. (뭐 쓰이지 않는 변수는 Eclipse의 warning으로 잡아낼수 있기는 합니다만)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지역변수를 쓰기 바로 전에 초기화하고, 선언과 함께 초기화 하는 좋습니다. 즉 가능한 변수가 처음 사용되는 곳의 가장 근접한 위치에서 선언되고 초기화되어야 한다는 말이죠. 변수의 수명을 가능한 짧게 유지하라는 말로도 표현됩니다. 하지만 많은 개발자들은 메서드의 처음에서 C언어처럼 쭉 변수를 선언하고 시작하고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;반복문에서는 while보다는 for를 쓰는 것이 변수의 범위관리에 유리합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보통 iterator와 while이 아래와 같이 많이 쓰이고 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Iterator i = c.iterator();

while (i.hasNext())\{

 doSomething(i.next());

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 것을 for문으로 쓴다면 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;for (Iterator i = c.iterator() ; i.hasNext(); ) {

 doSomething(i.next());

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;작은 차이지만 Iterator i는 for block을 벗어나는 순간  잊어버려도 되는 것이니 block 밖에서 개발자의 머리는 조금이나마 가벼워 질 수 있습니다. 이것은 캡슐화의 원칙인 class의 맴버 중 밖에서 볼 필요 없는 것들은 private으로 선언해야 하는 이유와 일맥상통합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그렇다면 다음의 경우는 어떠할 까요?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;1.루프밖에서 list에서 꺼내서 담을 변수 선언&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    static int countOfIncluded(List list, String str){
        int count = 0;
        String element;
        for (int i=0,n=list.size();i&amp;lt;n;i++)\{
           _*element = (String) list.get(i);*_
            if (element.indexOf(str)!= -1 ) count++;
        }
        return count;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2.루프안에서 list에서 꺼내서 담을 변수 선언&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;    static int countOfIncluded(List list, String str){
        int count = 0;

        for (int i=0,n=list.size();i&amp;lt;n;i++){
            String element = (String) list.get(i);
            if (element.indexOf(str)!= -1 ) count++;
        }
        return count;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많은 분들이 1번과 같이 코드를 작성하고 있고, 1번이 성능이 좋다는 &apos;믿음&amp;#8217;을 가지고 계실 것입니다. 실제로 한 번 테스트 해볼까요? 크기가 10000개인 리스트를 생성해서 100번씩 반복해서 실행시간을 찍어보는 코드를 만들어보았습니다. 1번 방법은 &lt;a href=&quot;http://benelog.springnote.com/pages/386996/attachments/166292&quot;&gt;ListReader1.java&lt;/a&gt;, 2번 방법은 &lt;a href=&quot;http://benelog.springnote.com/pages/386996/attachments/166293&quot;&gt;ListReader2.java&lt;/a&gt;, 시간을 찍어보는 코드는 &lt;a href=&quot;http://benelog.springnote.com/pages/386996/attachments/166297&quot;&gt;LoopTester.java&lt;/a&gt; 로 첨부하였습니다. 이런 비교 시에는 실행순서에 따라서도 실행시간이 영향을 받으므로 1,2,2,2,1,1,2의 순서로 몇번씩 사이에 걸린 시간을 밀리세컨드로 출력하게했습니다. 실행결과는 다음과 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;test1:밖에 선언 2243
test2:안에 선언 2153
test2:안에 선언 2184
test1:밖에 선언 2253
test1:밖에 선언 2213
test2:안에 선언 2123&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;거의 차이가 없거나 오히려 안에 선언한 쪽이 미묘하게 빠르기도 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이번에는 &lt;code&gt;javap -c&lt;/code&gt; 를 이용해서 컴파일된 byte 코드를 분석해 보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;&amp;gt; javap -c ListReader1

Compiled from &quot;ListReader1.java&quot;
public class looptest.ListReader1 extends java.lang.Object{
public looptest.ListReader1();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object.&quot;&amp;lt;init&amp;gt;&quot;:()V
   4:   return

static int countOfIncluded(java.util.List,java.lang.String);
  Code:
   0:   iconst_0
   1:   istore_2
   2:   iconst_0
   3:   istore  4
   5:   aload_0
   6:   invokeinterface #2,  1; //InterfaceMethod java/util/List.size:()I
   11:  istore  5
   13:  iload   4
   15:  iload   5
   17:  if_icmpge       50
   20:  aload_0
   21:  iload   4
   23:  invokeinterface #3,  2; //InterfaceMethod java/util/List.get:(I)Ljava/la
ng/Object;
   28:  checkcast       #4; //class String
   31:  astore_3
   32:  aload_3
   33:  aload_1
   34:  invokevirtual   #5; //Method java/lang/String.indexOf:(Ljava/lang/String
;)I
   37:  iconst_m1
   38:  if_icmpeq       44
   41:  iinc    2, 1
   44:  iinc    4, 1
   47:  goto    13
   50:  iload_2
   51:  ireturn

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;javap -c ListReader2
Compiled from &quot;ListReader2.java&quot;
public class looptest.ListReader2 extends java.lang.Object{
public looptest.ListReader2();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object.&quot;&amp;lt;init&amp;gt;&quot;:()V
   4:   return

static int countOfIncluded(java.util.List,java.lang.String);
  Code:
   0:   iconst_0
   1:   istore_2
   2:   iconst_0
   3:   istore_3
   4:   aload_0
   5:   invokeinterface #2,  1; //InterfaceMethod java/util/List.size:()I
   10:  istore  4
   12:  iload_3
   13:  iload   4
   15:  if_icmpge       49
   18:  aload_0
   19:  iload_3
   20:  invokeinterface #3,  2; //InterfaceMethod java/util/List.get:(I)Ljava/la
ng/Object;
   25:  checkcast       #4; //class String
   28:  astore  5
   30:  aload   5
   32:  aload_1
   33:  invokevirtual   #5; //Method java/lang/String.indexOf:(Ljava/lang/String
;)I
   36:  iconst_m1
   37:  if_icmpeq       43
   40:  iinc    2, 1
   43:  iinc    3, 1
   46:  goto    12
   49:  iload_2
   50:  ireturn

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/java/variable-scope-byte-code-diff.jpg&quot; alt=&quot;variable scope byte code diff&quot; title=&quot;Bytecode 비교&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;노란 줄이 많은 것은 라인수가 1라인 차이가 나고, local variable이 저장되는 공간의 index번호가 달라서입니다. 내용을 보시면 거의 똑같이 실행되고 있는 것을 알 수 있으실 것입니다.  루프가 도는 goto문장을 봐도 (왼쪽의 47라인과 오른쪽의 46라인) 같은 곳으로 (13라인과 12라인)으로 이동을 하기 때문에 특별히 오른쪽 예제2의 경우가 더 일을 하는 것은 없습니다. 다만 3번째 라인 istore n 이였던 것이 istore_n 으로 바뀌는 등 언더바(_)가 들어간 부분이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;각각의 명령어의 의미는 다음 링크를 확인해 보시면 나와있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html#istore&quot;&gt;istore&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html#istore_n&quot;&gt;istore_n&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html#iload&quot;&gt;iload&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html#iload_n&quot;&gt;iload_n&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecIX.fm9.html&quot;&gt;명령어의 인덱스페이지&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;내용을 보면 istore과 istore_n은  전자가 암시적이라는 것만 빼고는 같다고 나옵니다. (Each of the istore_&amp;lt;n&amp;gt; instructions is the same as istore with an index of &amp;lt;n&amp;gt;, except that the operand &amp;lt;n&amp;gt; is implicit. ) iload의 경우도 마찬가고요.  어떤 차이가 있을까 해서 검색해 봤더니 아래와 내용을 발견했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;istore_&amp;lt;n&amp;gt;&apos; is functionally equivalent to &apos;istore &amp;lt;n&amp;gt;&apos;, although it is typically more efficient and also takes fewer bytes in the bytecode&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.google.com/search?q=cache:SwAZqu5poI0J:www.cat.nyu.edu/%7Emeyer/jvmref/ref&amp;#8212;&amp;#8203;37.html+istore_%3Cn%3E+more&amp;amp;hl=ko&amp;amp;ct=clnk&amp;amp;cd=1&amp;amp;gl=kr&quot;&gt;원본링크&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;istore_n 쪽이  오히려 효율적이라는 말이 나와 있습니다. 만약 iload_n도 마찬가지라면 goto가 찾아가는 라인에서는 iload_n이 있는 예제2가 더 효율적인 코드일 수도 있다는 것입니다. 어쨓든 이런 작은 차이를 접어둔다면 코드가 하는 일의 절차는 차이가 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;결국 &lt;strong&gt;&quot;루프 안에서 반복되는 변수 선언을 밖으로 빼는 것은 성능상에 아무런 이점이 없고 소스에서 변수의 유효범위만 늘어나게 한다. &quot;&lt;/strong&gt; 는 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;여기서 이런 말을 하실 분이 계실 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;원래는 변수를 loop안에 생성하는 루프가 돌 때마다 String 객체의 참조를 저장하기 위한  공간이 따로 할당되는 것인데 위의 경우는 JVM에서 최적화를 해 준 것 아니냐?   JVM에 따라서 이런 최적화가 안 되는 버전도 있을 수가 있는데 개발자는 어떤 JVM에서도 한번만 선언이 되도록 루프 밖에 변수를 선언해야 하지 않겠냐? &quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그러나 &lt;a href=&quot;http://www.informit.com/bookstore/product.asp?isbn=0201432943&amp;amp;aid=9f15cdfa-4e22-40dc-bfc9-cdc6322be0fd&amp;amp;rl=1&quot;&gt;&lt;em&gt;Java™ Virtual Machine Specification, The, 2nd Edition&lt;/em&gt;&lt;/a&gt; 이라는 책을 보면 다음과 같은 내용이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The sizes of the local variable array and the operand stack are determined at compile time and are supplied along with the code for the method associated with the frame .&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 문장은 메서드가 호출될 때 생성되는 저장공간인 frame에 대한 설명에서 인용한 것입니다. (원문링크 : &lt;em&gt;&lt;a href=&quot;http://java.sun.com/docs/books/vmspec/2nd-edition/html/Overview.doc.html#15722&quot; class=&quot;bare&quot;&gt;http://java.sun.com/docs/books/vmspec/2nd-edition/html/Overview.doc.html#15722&lt;/a&gt;&lt;/em&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;풀이하면  메서드 안의 local variable들의 값들은 고정된 크기의 배열에 저장되고 그 배열의 크기는 compile시에 결정된다는  내용이 있습니다. 즉 한 메서드 안에서 사용할 local variable이 저장될 공간의 갯수는 이미 compile 시에 정해져 있는 것이지 동적으로 변하는 것이 아니라는 말입니다. 만약 위의 소스에서 list의 갯수에 따라서 local variable의 값이 저장되는 공간(객체가 저장되는 공간을 가리키는 말이 아닙니다.)이 달라진다면 위의 설명과 모순되는 일입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://forum.java.sun.com/thread.jspa?threadID=707455&amp;amp;messageID=4098210&quot; class=&quot;bare&quot;&gt;http://forum.java.sun.com/thread.jspa?threadID=707455&amp;amp;messageID=4098210&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;비슷한 설명과 논쟁이 인터넷의 여러 곳에서 이미 벌어졌으니 아래의 링크를 읽어보셔도 재밌을 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;local variables in Java : &lt;a href=&quot;http://rmathew.blogspot.com/2007/01/local-variables-in-java.html&quot; class=&quot;bare&quot;&gt;http://rmathew.blogspot.com/2007/01/local-variables-in-java.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Myth - Defining loop variables inside the loop is bad for performance : &lt;a href=&quot;http://livingtao.blogspot.com/2007/05/myth-defining-loop-variables-inside.html&quot; class=&quot;bare&quot;&gt;http://livingtao.blogspot.com/2007/05/myth-defining-loop-variables-inside.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://forum.java.sun.com/thread.jspa?threadID=707455&amp;amp;messageID=4098210&quot; class=&quot;bare&quot;&gt;http://forum.java.sun.com/thread.jspa?threadID=707455&amp;amp;messageID=4098210&lt;/a&gt;http://forum.java.sun.com/thread.jspa?threadID=707455&amp;amp;messageID=4098210&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://weblogs.java.net/blog/ddevore/archive/2006/08/declare_variabl_1.html&quot; class=&quot;bare&quot;&gt;http://weblogs.java.net/blog/ddevore/archive/2006/08/declare_variabl_1.html&lt;/a&gt;http://weblogs.java.net/blog/ddevore/archive/2006/08/declare_variabl_1.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://www.theserverside.com/news/thread.tss?thread_id=41857&quot; class=&quot;bare&quot;&gt;http://www.theserverside.com/news/thread.tss?thread_id=41857&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://weblogs.java.net/blog/ddevore/archive/2006/08/declare_variabl_1.html&quot; class=&quot;bare&quot;&gt;http://weblogs.java.net/blog/ddevore/archive/2006/08/declare_variabl_1.html&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;1,2번 링크가 바이트코드 역어셈블 결과와 함께 내용을 잘 풀어서 설명하고 있습니다. 3번 링크에서 Dru Devore는 primitive type일 때는 똑같고 Object type일때는 다르다는 결론을 쓰고 있으나, 사실 그가 든 object type의 예제는 &apos;객체를 한번 생성하느냐&apos; 대 &apos;반복해서 생성하느냐&apos;의 문제이므로 경우가 다릅니다. 3번 링크의 댓글에서 다른 사람들도 그 점을 지적하고 있고 2번 링크의 글에서도 관련 설명이 있습니다. 어쨓든 루프밖에 있으나 안에 있으나 바이트코드의 실행과정은 같고 성능은 차이가 없다는 내용은 모든 페이지에 다  있군요.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 오해들은 전에 &lt;a href=&quot;http://benelog.springnote.com/pages/36726&quot;&gt;&lt;em&gt;Java의 호출은 pass by value&lt;/em&gt;&lt;/a&gt; 에서 말했던 객체선언에 대한 오해와 어느 정도 관련이 있다고 봅니다. 객체를 생성이나 사용을 안 하더라도 선언 자체만으로 저장을 위한 큰 공간이 할당된다고 믿는 경우가 많은 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전에 이런 코드를 본적이 있습니다. 클래스의 멤버 변수로 임시 객체를 생성해 놓고 그것을 여러개의 메서드에서 같이 쓰는 코드였었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;Class MyClass {
    String  temp;

    void work(List list){
        for (int i=0; i = list.size(); i++)\{
            temp = (String) list.get(i);
            //기타...
        }
    }

    void play(List list)\{
        for (int i=0; i = list.size(); i++)\{
            temp = (String) list.get(i);
            //기타...
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이런 코드를 짰던 사람도 경력도 어느 정도 되고, 실력도 있어보이는 사람이였죠. 아마 이 개발자는 이런 구조가 temp 객체가 저장될 공간을 상당히 아꼈다는 뿌듯함을 가지고 있었을 지도 모릅니다. 그러나 객체는 Heap 메모리에 있는 것이기에 지역변수가 선언이 하나 덜 되었다고 해서 객체가 줄어든 것은 아닙니다. 그리고 여기서는 list에서 받아오지 새로 생성되는 객체는 없습니다. 더군다나 설계상 의미로도 temp는 Class의 멤버로써 아무런 의미가 없습니다. 성능측면에서 봐도  클래스 멤버 변수는 지역변수에 비해서 더 비용이 큽니다. (참고자료 2: 자바 퍼포먼스 튜닝)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;한편으로는 변수의 범위를 줄인다고 해서 반복적으로 Loop 안에서 실행해줄 필요가 없는 작업까지 loop안으로 끌고 들어가서는 안되겠죠. 다음과 같은 코드를 본적이 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;for (int i=0, n=userList.size(); i&amp;lt;n;i++)\{

    UserBiz biz = (UserBiz) lookup(UserBiz.ROLE);#*_ //비지니스 컴포넌트 객체를 얻어온다.
    User user = (User)userList.get(i);
    biz.add(user;
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 코드에서 비지니스 컴포넌트를 얻어오는 부분은 반복적으로 수행될 필요가 없으므로 loop 밖으로 나가는 것이 맞습니다.  위의 biz 객체를 loop안에서만 쓴다고 하여도 위와 같이 해당 코드가 안으로 들어가 있다면 한번만 얻어오는 되는 객체를 얻어오는 작업이 계속 반복될 것 입니다. 사실 위의 경우는 비지니스 객체에서 List를 받아서 처리하는 메서드를 하나 더 만드는 것이 나을 것 같네요. 아키텍쳐에 따라 다르지만 비지니스 객체의 호출은 비싼 작업일 경우가 많고 그런 호출의 횟수는 최대한 줄일수록 좋겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;개선된_for문&quot;&gt;개선된 for문&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java 5의 개선된 for문을 아시는 분이라면 아래의 방법을 많이 쓰실 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;ArrayList&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;Integer&amp;gt;();&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;for (Integer i : list) \{ &amp;#8230;&amp;#8203; }&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;( &lt;a href=&quot;http://java.sun.com/developer/technicalArticles/releases/j2se15/&quot;&gt;J2SE 5.0 in a Nutshell&lt;/a&gt; 에서)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보기에도 이쁘고, 변수의 유효범위가 loop을 벗어나지도 않는 좋은 문법이네요. ^^ 프로젝트에서 Java 5 이상을 쓰시고 계신다면 변수를 loop안에 넣느냐로 논쟁하기 전에 개선된 for문을 먼저 적용해 보는것이 나을 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;마치며&quot;&gt;마치며&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 내용을 묶어서 결론을 내리자면, &quot;&lt;strong&gt;&lt;em&gt;loop안에서만 쓰는 변수 선언을 밖으로 빼는 것은 성능에 아무런 영향이 없으며, 굳이 성능을 개선하고 싶다면 RandomAccess 인터페이스의 고려, 반복되는 list크기 계산의 제거 등을 먼저 신경쓰라. 그리고 Java5을 쓰고 있다면 개선된 for문을 쓰라.&lt;/em&gt;&lt;/strong&gt;&quot;는 것으로  정리하고 싶습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;사실 성능에 대한 고려는 병목이 발견될 때 필요하다면 하는 것이 바람직한 순서겠죠. 개발자는 알아보기 싶고, 관리하기 쉬운 코드를 짜는 것에 먼저 집중을 해야 할 것입니다. 그리고 고용량의 CPU가 돌아가는 환경이라면 개발자가 성능을 고려해서 짠 코드와 그렇지 않은 코드의 차이도  대부분 사람이 인지하지 못하는 정도의 미미한 것일 가능성이 높습니다. (TA팀 이상민 선임님의 테스트와 조언). 그런 점에서 본다면 이 글에서 가장 강조되어야 할 내용은 &apos;변수의 유효범위의 최소화&amp;#8217;라고 생각됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다른 분들이 추가로 자료를 찾는 번거로움을 덜어 드릴려고 관련 자료를 되도록 인용하고 링크를 걸었습니다. 그렇게는 했어도 혹시나 제가 잘못 이해해서 오류가 있는 부분이 있는지도 모르겠습니다.  고수분들은 그런 부분을 발견하신다면 즉각 지적해 주시기 바랍니다~ ^^&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 이 글은 계속적으로 갱신해서 개선을 하고 있는 중입니다. 제가 가입해 있는 커뮤니티 사이트 중 두 곳에도 이 글을 올렸는데, 거기에서 조언을 주신 많은 분들께도 감사드립니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Java API문서와 링크외의 책에서 참고한 내용입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Effective Java , Joshua Bloch저 이해일역&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;7장 프로그래밍 일반 - 항목29 - 지역변수의 유효범위를 최소화하라.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;자바퍼포먼스 튜닝, Jack Shirazi 저 서민구 역&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;11장 적절한 자료구조와 알고리즘&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;질의 최적화 - 불필요한 반복적 메서드 호출 제거&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RandomAccess 인터페이스&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;6장 예외 단언, 캐스팅, 변수 - 변수&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Code completed 2nd Edtion, Streve McConnell 저 서우석 역&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;10장 변수사용시 일반적인 문제 - 10.3 변수의 초기화에 대한 지침&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Head First Design Pattern&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;9장 이터레이터와 컴포지트 패턴&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>구글 검색 기록과 스위칭 코스트</title>
      <link>https://blog.benelog.net//1375660.html</link>
      <pubDate>Tue, 31 Jul 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1375660.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많은 분들이 그렇듯이 저도 생활정보는 네이버로 찾고  일과 관련된 정보나  웹을 구석구석 뒤지고 싶을 때는 google을 쓰고 있습니다.  google을 써오기는 해도  &quot;검색기록&quot;이라고는 것을 신경도 안 쓰고 있었는데, 얼마 전에 한번 찍어보니 꽤나 신기하더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.google.co.kr/history/&quot; class=&quot;bare&quot;&gt;http://www.google.co.kr/history/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 기능을 의식을 하고 쓴 것은 아니였는데 , 제가 어떤 키워드로 검색을 했는지 오래전의 기록부터 쫙 나오니깐 기분이 묘했습니다. 상위질문, 상위사이트, 상위클릭, 월별 일별 통계까지 나오니 좀 섬뜻하기도 했어요  삭제하고 싶은 키워드도 있는데  어떻게 삭제하는지 모르겠습니다 ^^;다음부터는 기록에 남기고 싶지 않는 검색어는 &quot;일시중지&quot;를 선택하고 검색해야겠군요. 얼마 전에 &quot;&lt;a href=&quot;http://blog.daum.net/suk5kyu/11605503&quot;&gt;구글, 사용자 검색기록 보관기간 18개월로 단축 &lt;/a&gt;&quot; 이라는 기사를 보았었는데, 정말 이것이 축적되면  무서운 도구가 될 수도 있을 것 같아요. 찜찜하신 분들은 구글에서 로그인하지 않은 상태로 검색하거나  기록 기능을 꺼야겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/google/google-search-history1.jpeg&quot; alt=&quot;google search history1&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/google/google-search-history2.jpeg&quot; alt=&quot;google search history2&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;검색기록의 첫 페이지에 나와 있는 다음의 문구가 눈에 띕니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;맞춤 검색은 Google 검색을 개선한 기능으로서 이전의 검색을 토대로 귀하의 검색 결과를 표시합니다. 처음에는 큰 차이를 느낄 수 없지만 검색 기록이 축적되면서 맞춤 검색 결과가 지속적으로 향상됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이번달 16일, &lt;a href=&quot;http://www.joonj.com/&quot;&gt;jmirror&lt;/a&gt;님이 주최하신 &apos;&lt;a href=&quot;http://www.joonj.com/wordpress/archives/319&quot;&gt;차세대 검색에 대한 블러거 토론회&lt;/a&gt;&apos;에 참석했습니다. 저는 특별한 발언없이 경청만 하다 왔지만, 다른 분들의 깊이 있는 말씀을 듣고 생각을 정리해볼 수 있는 자리였습니다. 거기서 몇몇 분들이 &apos;검색의 개인화&amp;#8217;가 앞으로 필요하다고 말씀하셨습니다. 그 때 저는 그 실제 적용 사례가 생각나지 않았었는데, 바로 이 구글의 검색기록을 한 번 찍어보고 갔었더라면 위의 화면이 머리 속에서 연결이 되었을건데 하는 아쉬움이 지금에서야 듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위의 토론회에서 들은 말인데, 구글이 유투브 동영상 검색에서 야후를 야후의 동영상 검색을 누르는데 걸린 시간은 6주였고, 국내  3위 업체였던 네이버는 지식검색으로  야후,  엠파스를 4주만에 추월했다고 합니다. 그렇게 짧은 시간내에 그것이 가능했던 이유는 검색엔진을 바꾸는 것에 대한 이사비용(스위칭코스트)이 거의 없기 때문이 아닐까 합니다. 메일, 블로그,커뮤니티의 경우에는 서비스를 바꾼다면 데이터 이전, 바뀐 주소 홍보 등의 번거로움을 감수해야하는 것에 비해서 검색은 그렇지 않다는 것이죠. 그리고 검색결과의 상호비교가 쉽기 때문에  조금이라도 나은 점이 보이는 서비스로 지체없이 옮겨갈 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;검색에서도 구글 검색 기록과 같이 쌓여가는 데이터가 있고, 그것을 바탕으로 개인화된 서비스를 제공할 수 있다면  스위칭코스트를 낳을 수 있다고 봅니다. 그리고 개인 검색기록  데이터를 쌓고  그 것을 바탕으로 개인화된 검색결과를 내놓을 수 있는 기술은 쉽게 따라할수 있는 일이 아닐 것 같아요. 네이버에 있는 지식인의 자료, 광고, 알바가 정리한 정보도 큰 자산이지만  그것만으로 사용자를 완벽히 묶어둘 수는 없지 않을까요?&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>AOP 관련 용어 정리</title>
      <link>https://blog.benelog.net//1348296.html</link>
      <pubDate>Thu, 19 Jul 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1348296.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;부서원들과 같이 하는 세미나를 위해서 정리했었던 자료 중의 일부입니다. 대충 알고 있었던 것도 막상 누가 물어보면 한마디로 설명하는 것은 어렵다고 종종 느낍니다.  세미나 발표 준비를 하면서 모르면서 알고 있었다고 착각했던 부분을 많이 발견할 수 있어서 좋았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;약간 엉뚱한 이야기지만, 포스트를 작성하는 동안 &apos;그림1&amp;#8217;을 다시 보면서 최근 읽고 있는 &lt;a href=&quot;http://www.amazon.com/World-Flat-Updated-Expanded-Twenty-first/dp/0374292795/ref=pd_bbs_sr_1/105-2068841-6541209?ie=UTF8&amp;amp;s=books&amp;amp;qid=1184811573&amp;amp;sr=8-1&quot;&gt;&apos;The world is flat&lt;/a&gt;&apos;이라는 책의 내용이 생각났습니다. 그 책의 처음에는 기업의 아웃소싱에 대한 사례가 많이 나옵니다.  기업의 핵심 업무가 아니고 밖에서 할 수있는 업무들은 보다 싼 비용으로 처리할 수 있는 해외의 조직으로 넘기는 사례가 늘어나고 있다는 것이죠. 각 업무단의 코드에는 핵심로직만 남기고 공통적인 처리는 따로 정의해 둔 것을 weaving해서 쓴다는 내용과 왠지 비슷하게 보이더군요.  (그런데 책을 아직 1장 밖에 못 읽었어요)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;aop_용어&quot;&gt;AOP 용어&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://en.wikipedia.org/wiki/Aspect-oriented_programming&quot;&gt;AOP&lt;/a&gt; :보안, 예외등과 같이 프로그램에 처리해야 하는 다양한 관점을 분리(&lt;a href=&quot;http://en.wikipedia.org/wiki/Separation_of_concerns&quot;&gt;Separation of concerns&lt;/a&gt;)시키고 각각의 관점별로 구현을 한 후 이를 조합하여 완성된 프로그램을 만들고자 하는 개념. (참고자료1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;횡단관점(&lt;a href=&quot;http://en.wikipedia.org/wiki/Cross-cutting_concern&quot;&gt;crosscutting concerns&lt;/a&gt;) : 분리된 모듈로 작성하기 힘든 시스템 전반적으로 산재된 기능(예로 보안, 로그, 인증)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;핵심관점(main concern) : 해당시스템의 핵심가치와 목적이 드러난 관심영역 (primary concern, &lt;a href=&quot;http://en.wikipedia.org/wiki/Core_concern&quot;&gt;core concern&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;조인포인트(&lt;a href=&quot;http://en.wikipedia.org/wiki/Join_point&quot;&gt;join point&lt;/a&gt;) : 횡단 관심모듈의 기능이 삽입되어 호출될 수 있는 실행가능한 특정위치&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;포인트컷(&lt;a href=&quot;http://en.wikipedia.org/wiki/Pointcut&quot;&gt;point cut&lt;/a&gt;) : 조인포인트의 집합. 어떤 클래스의 어느 조인포인트를 사용할 것인지 결정하는 선택 기능. Code와 advice를 연결해주는 설정정보.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;어드바이스(&lt;a href=&quot;http://en.wikipedia.org/wiki/Advice_in_aspect-oriented_programming&quot;&gt;advice&lt;/a&gt;) : 각  조인포인트에서 행하는 행위, 삽입될 수 있는 코드. cross-cutting concern을 구현한 코드.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;조합(weaving) : 핵심관점코드와 어드바이스를 조합해 애플리케이션으로 완성하는 과정&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;기술적 용어로서의 asepct : Advice와 point-cut을 함께 지칭하는 단어&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;참고자료&quot;&gt;참고자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;마이크로소프트웨어 2005/11 테크니컬 칼럼 - 객체지향을 넘어 관점지향으로 AOP (이일민)   (&lt;a href=&quot;http://www.zdnet.co.kr/builder/dev/java/0,39031622,39147106,00.htm&quot; class=&quot;bare&quot;&gt;http://www.zdnet.co.kr/builder/dev/java/0,39031622,39147106,00.htm&lt;/a&gt; )&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;마이크로소프트웨어 2006/09 S/W로 여는 세상 - 관점지향의 개념잡기 -상황중심의 프로그래밍 (임백준) (&lt;a href=&quot;http://www.zdnet.co.kr/builder/dev/etc/0,39031619,39154322-1,00.htm&quot; class=&quot;bare&quot;&gt;http://www.zdnet.co.kr/builder/dev/etc/0,39031619,39154322-1,00.htm&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;3번 자료에 대한 반론 - &lt;a href=&quot;http://toby.epril.com/?p=229&quot; class=&quot;bare&quot;&gt;http://toby.epril.com/?p=229&lt;/a&gt; (이일민님의 블로그)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;마이크로소프트웨어 2007/03 Cover Story- OOP한계 극복패러다임 AOP (이일민,김백기,박재성, 류대원)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;한빛네트워크에 올라온 AOP소개 기사 : &lt;a href=&quot;http://network.hanb.co.kr/view.php?bi_id=968&quot; class=&quot;bare&quot;&gt;http://network.hanb.co.kr/view.php?bi_id=968&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;위키페디아&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>로그인별 process 숫자를 집계하는 sybase 쿼리</title>
      <link>https://blog.benelog.net//1328079.html</link>
      <pubDate>Tue, 10 Jul 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1328079.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;sa 계정으로 로그인해서,&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;select
    p.hostname,
    p.ipaddr,
    p.cnt,
    (select l.name from syslogins l where l.suid = p.suid) name
from
    (select s.hostname, s.ipaddr, s.suid,
        count(*) cnt from sysprocesses s
group by s.hostname, s.ipaddr ,s.suid ) p&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;현재 어느 로그인 아이디에서 많이 프로세스가 붙어있는지 볼 때 쓰면 됩니다.
내장 함수 중에 비슷한게 있는지 모르겠네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;간단한 내용이지만 필요하신 분들이 검색엔진을 타고 오셔서 찾아가시길 바라는 의도에서 올립니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>HTML &amp;amp; CSS 정리해본 예</title>
      <link>https://blog.benelog.net//1314658.html</link>
      <pubDate>Wed, 4 Jul 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1314658.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;제가 있는 프로젝트의 UI 표준은  2004년 10월경에 처음 정해졌습니다.
연속 사업 중 첫 번째 사업 때 정해진 틀을 수정없이 Copy&amp;amp;paste로 입혀서 쓰고 있는 중이였습니다.
디자이너가 HTML로 파일을 만들어 주었을 당시에는 팀원 중에 웹표준에 대해서 신경을 썼던 사람이 없었습니다.
지금 다시 들여다보니 CSS를 잘 정의했으면 훨씬 깔끔하게 중복없이 만들수 있었던 코드가 많이 보입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저도 작년에 파주시 프로젝트에서 웹표준에 맞춰서 개발을 하고 있다는 말을 듣기 전까지는 스스로 css를 정리해볼 생각은 없었습니다.
그런 작업은 디자이너의 영역이라고 안이한 생각을 했었습니다.
새로 프로젝트 들어가면 깔끔하게 UI표준 만들어주는 사람을 만나기를 바라는 마음만 있을 뿐이였습니다.
그러다가 정동인씨나 최효근씨 같은 후배들에게 배우고 느낀 바가 많아서 저도 현재의 프로젝트에서도 서서히 적용하겠다는 의지를 가지게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이번에 고객이 다른 업무와 독립적으로 쓰여질 프로그램을 요청했는데, 그 페이지에는 새로운 CSS를 만들어서 적용하고 싶었습니다.
현재 프로젝트에 남은 3명의 인원만으로는 모든 페이지를 다 갈아 엎는 것은 무리라고 생각했었는데 적당한 시작점을 찾은 것이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;처음 수정한 부분은 화면의 큰 제목과 하위 제목이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/markup/header-sample.gif&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;와 같이 보여지는데, 기존 코드로는&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&amp;lt;table border=&quot;0&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; width=&quot;100%&quot;&amp;gt;
 &amp;lt;!-- 제목 --&amp;gt;
 &amp;lt;tr&amp;gt;
  &amp;lt;td&amp;gt;
   &amp;lt;table border=&quot;0&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; width=&quot;100%&quot;&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td class=&quot;title1&quot;&amp;gt;&amp;lt;img src=&quot;/image/title_header.gif&quot; align=&quot;absmiddle&quot;&amp;gt;&amp;amp;nbsp;감사사항 목록&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td class=&quot;tilte_space&quot;&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td class=&quot;title_line&quot;&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td class=&quot;tilte_space&quot;&amp;gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
   &amp;lt;/table&amp;gt;
  &amp;lt;/td&amp;gt;
 &amp;lt;/tr&amp;gt;
 &amp;lt;!-- 조회조건 --&amp;gt;
 &amp;lt;tr&amp;gt;
  &amp;lt;td&amp;gt;
   &amp;lt;table border=&quot;0&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; width=&quot;100%&quot;&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;td align=&quot;left&quot; class=&quot;title_small&quot;&amp;gt;
            &amp;lt;img src=&quot;/image/title_small.gif&quot; align=&quot;absmiddle&quot;&amp;gt;&amp;amp;nbsp;조회조건
        &amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
   &amp;lt;/table&amp;gt;
  &amp;lt;/td&amp;gt;
 &amp;lt;/tr&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;로 나와있는 부분입니다. CSS를 새로 만들고 나니 마크업은 아래와 같이 딱 두줄로 바꿀수가 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;  &amp;lt;h1&amp;gt;감사사항 목록&amp;lt;/h1&amp;gt;
  &amp;lt;h2&amp;gt;조회조건&amp;lt;/h2&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CSS에 다음과 같이 배경이미지, 선 등을 넣어서 정리했습니다.
매 페이지마다 반복되는 글자 앞의 이미지, 선과 여백을 위한 &lt;code&gt;table&lt;/code&gt;, &lt;code&gt;tr&lt;/code&gt;, &lt;code&gt;td&lt;/code&gt; 문장들이 모두 정리됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;h1 {
  font-size:14px; color:#5C5C5C;
  font-family: 돋움, arial;
  font-weight:bold;
  height:14px;
  padding-left: 20px;
  padding-bottom: 10px;
  margin: 10px 10px 2px 10px;
  background-image: url(/image/title_header.gif);
  background-position: 0% 10% ;
  background-repeat: no-repeat;
  border-color:#C0C3C6;
  border-width:1px;
  border-bottom-style: solid;
}

h2 {
  font-family: 돋움, arial;
  font-weight:bold;
  font-size:12px;
  height:13px;
  color: #4f4f30;
  padding-left: 20px;
  margin-bottom: 4px
  padding-bottom: 10px;
  margin: 10px;
  background-image: url(/image/title_small.gif);
  background-position: 0% 0% ;
  background-repeat: no-repeat;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 새로 개발되는 화면에 좌측 메뉴부분이 있는데,  css에서   float:left 선언을 이용하니 table없이 레이아웃을 잡을 수가 있었습니다. 웹표준을 설명하는 자료에서 많이 드는 예제이죠. 반복되는 class 지정에도 선택자를 잘 활용하면 클래스 지정없는 태크만으로 처리할 수 있는 부분이 많아보였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이제 기초적인 것을 적용시킨 것 뿐이고 제가 공부할 것이 아직 많아서 갈 길이 멀다는 기분이에요. 그래도 간결한 jsp를 보니 몇달만에 방청소 했을 때보다 더 속이 시원합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그동안 UI단 코드의 중복에는 지나치게 관대하게 살아왔었다고 반성했습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;웹표준_관련_권장_자료&quot;&gt;웹표준 관련 권장 자료&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(부서 후배 최효근씨가 추천해준 자료입니다)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;한국소프트웨어진흥원의 실전웹표준가이드&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Head First HTML(XHTML, CSS) -한빛미디어-&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;실용예제로 배우는 웹표준 -에이콘-                // 본격적인 활용기&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;웹2.0을 이끄는 방탄웹 -에이콘-                    // 아이디어가 넘치는 책&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS 마스터 전략 -에이콘-&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS designing without tables -sitepoint-(원서)    // 무릎을 탁 치게 되는 책&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CSS the css anthology -sitepoint-(원서)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>코드와 예술</title>
      <link>https://blog.benelog.net//1230312.html</link>
      <pubDate>Fri, 1 Jun 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1230312.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.creativecommons.or.kr/&quot;&gt;CCK(Creative Commons Korea)&lt;/a&gt;의 &lt;a href=&quot;http://ccsalon.tistory.com/&quot;&gt;&apos;Code can be an art.&apos; 행사 소식&lt;/a&gt;을 처음 들었을 때, 생각났던 자료들을 정리해서 올려봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음은 임백준님의 책 &apos;누워서 읽는 알고리즘&apos; 중 &apos;문학적 프로그래밍&amp;#8217;이라는 장에서 소개 된 내용입니다.  Art of computer proramming으로 유명한 카누스 교수가 1974년 11월 미국 샌디에이고에서 열린 ACM(Association for computer Machinery) 연례회의에서 &apos;예술로써의 컴퓨터 프로그래밍(Computer programming as an Art)라는 제목의 강연을 했다고 합니다. 그 강연은 다음과 같은 말로 끝이 맺어졌습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;우리는 컴퓨터 프로그래밍을 하나의 예술로 생각한다. 그것은 그 안에 세상에 대한 지식이 축적되어 있기 때문이고 기술과 독창성을 요구하기 때문이고 그리고 아름다움의 대상을 창조하기 때문이다. 어렴풋하게나마 자신을 예술가라고 의식하는 프로그래머는 스스로 하는 일을 진정으로 즐길 것이며, 또한 남보다 더 훌륭한 작품을 내놓을 것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;We have seen that computer programming is an art, because it applies accumulated knowledge to the world, because it requires skill and ingenuity, and especially because it produces objects of beauty. A programmer who subconsciously views himself as an artist will enjoy what he does and will do it better. Therefore we can be glad that people who lecture at computer conferences speak about the state of the Art.&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;위 강연의 PDF파일을 찾았는데 7페이지 밖에 안 되는군요. 이정도면 출력해서 퇴근길에 읽어볼만 한것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://faq.ktug.or.kr/faq/LiterateProgramming?action=download&amp;amp;value=knuth-turingaward.pdf&quot; class=&quot;bare&quot;&gt;http://faq.ktug.or.kr/faq/LiterateProgramming?action=download&amp;amp;value=knuth-turingaward.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 1992년에 출판된 책 &apos;문학적 프로그래밍(Literate Programming, Center for the Study of Language and Information,1992)은 카누스 교수의 논문과 &apos;생각하는 프로그래밍&amp;#8217;의 저자 존베틀리의 칼럼이 묶여진 책입니다. 이 책의 서문은 다음과 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;컴퓨터 프로그램을 작성하는 일은 재미있다. 그리고 잘 작성된 프로그램을 읽는 것도 재미있다. 세상에서 가장 즐거운 일 중 하나는 여러분이 작성한 컴퓨터 프로그램을 다른 사람들 혹은 여러분 자신이 읽고 기쁨을 얻는 것이다. 컴퓨터 프로그램은 또한 유용한 작업을 수행할 수도 있다. 세상에서 가장 큰 성취감을 맛보는 순간은 여러분이 창조한 무엇이 사회의 부와 진보에 기여한다는 사실을 깨닫는 순간이다. 어떤 사람들은 컴퓨터 프로그램을 작성함으로써 돈을 벌기도 한다. 따라서 프로그래밍은 세 가지 측면에서 결실을 맺는 행위다. 미학적으로, 인류학적으로 그리고 경제적인 면이 바로 그러한 결실에 해당한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;카누스 교수의 관련 논문도 PDF파일로 구할 수 있었습니다. &lt;a href=&quot;http://faq.ktug.or.kr/faq/LiterateProgramming?action=download&amp;amp;value=knuthweb.pdf&quot; class=&quot;bare&quot;&gt;http://faq.ktug.or.kr/faq/LiterateProgramming?action=download&amp;amp;value=knuthweb.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;얼마 전에 &apos;문장강화&apos;(이태준 저)라는 글쓰기에 관한 책을 읽었습니다. 1940년에 처음 나온 책임에도 불구하고 아직까지  이 책을 넘어서는 글쓰기 공부책이 없다고 말하는 사람이 많더군요.  책에 들어간 예문이 다  옛날 글이라는 것이 아쉽지만, 그럼에도 이 책이 그런 찬사를 받는 것에는 그럴만한 이유가 있다고 느껴졌습니다.  제가 제일 감탄을 했던 내용은 글을 고쳐서 다듬는 &apos;퇴고&amp;#8217;에 대한 예시였습니다. 저자는 얼핏 보기에 별 흠이 없어 보이는 문장을 점차적으로 고치는 과정을 보여줍니다. 그 부분을 읽을 때 저는 마틴파울러가 쓴 책 &apos;리팩토링&apos; 제1장을 읽을 때와 굉장히 흡사한 느낌을 받았습니다. 퇴고가 코드를 리팩토링하는 것과 비슷하게 느껴진 것이죠. 유심히 안 보면  이상 없어 보이는 코드나 문장들이  여러 관점에서 따져가니 고칠 점이 투성이인  것들이었습니다. 그리고 고쳐진 최종결과물들은 처음에 비해서 훨씬 명확하고 이쁜 코드와 문장들이였고요. 그때 다시 한번 코드와 문학, 넓게는 예술과 유사점이 있다는 것을 떠올렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;많은 분들이 아시겠지만 &apos;해커와 화가&amp;#8217;라는 책에서는 미술과 프로그래밍의 유사점을 이야기하고 있습니다. 이 책의 저자인 폴그레이엄은 특이하게도 미술과 전산을 같이 전공한 사람입니다. 그 예 중의 하나가,세계적인 명화들을 엑스레이 투시해보면 그 아래에는 수많은 스케치 후에 많은 시행착오를 거친 선의 흔적들이 남아있다고 합니다. 코딩도 마찬가지의 과정을 거치는 것이고, 그것을 죄악시 할 필요가 없다고 저자는 이야기합니다. 저도 자바 코딩을 할 때 처음에는 전체의 클래스 선언과 비어있는 메서드부터 만들어서 틀을 잡은 다음에 나중에 메서드 안의 내용을 구체적으로 채워넣을 넣는 순서로 진행을 합니다. 그러고보니 그것이 스케치하는 것과 비슷하게도 느껴지더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;재미있게도 이외수님의 &apos;글쓰기의 공중부양&amp;#8217;이라는 책에도 비슷한 내용이 나옵니다. 여기서 저자는 글을 쓸 때 처음부터 한꺼번에 모든 부분을 구체적으로 쓸려고 하지말고 글 전체의 틀을 구어체로 스케치한 후에 나중에 세부묘사를 채워넣으라고 합니다. 인물화를 그릴 때 처음부터 이목구비의 정밀묘사를 하면서 그려나간다면 나중에 이목구비가 따로 노는 그림이 된다는 비유가 나오더군요.  이렇게 그림,글, 코딩이 다 통하는게 있나봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전 한때는 코딩 혹은 프로그래밍은 예술이라는 말이 지나치게 순진하다고 느껴졌었기에 밖으로 꺼내기에는 민망하다고 느낀 적도 많았습니다. 개발자 커뮤니티 사이트에 이런 글이 올라간다면 &apos;코딩은 노가다인 걸 아직 모르냐?&apos;,&apos;고생을 덜해봤냐?&apos; 같은 욕을 바가지로 퍼먹여줄 글이 될 수도 있겠죠 ^^; 하지만 프로그래밍을 하면 할수록 그 것이 진실이라는 생각만 강해지고만 있어요. 위에서 유명한 아저씨들도 다 그렇게 이야기하고 있는 것 보니 위안이 되네요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;인용자료_출처&quot;&gt;인용자료 출처&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&apos;KTUG(Korean TeX User Group)의 문학적 프로그래밍에 관한 전반적인 소개&apos; &lt;a href=&quot;http://faq.ktug.or.kr/faq/LiterateProgramming&quot; class=&quot;bare&quot;&gt;http://faq.ktug.or.kr/faq/LiterateProgramming&lt;/a&gt;
ACM 강연의 원문, PDF 파일&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;누워서 읽는 알고리즘, 임백준
( ACM 강연의 번역내용, &apos;문학적 프로그래밍&apos; 서문 번역내용&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>Code can be an art 행사 참석 후기</title>
      <link>https://blog.benelog.net//1231656.html</link>
      <pubDate>Fri, 1 Jun 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1231656.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;지난 2007/05/19(토)에  &lt;a href=&quot;http://www.creativecommons.or.kr/&quot;&gt;CCK(Creative Commons Korea)&lt;/a&gt;에서 주최한  &lt;a href=&quot;http://ccsalon.tistory.com/&quot;&gt;ccSalon&lt;/a&gt; 행사가 있었습니다 . 이날의 행사 제목은 &apos;Code can be an art&amp;#8217;였었죠&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;관련 글과 같이 올릴려고 하다 보니 거의 2주가 지나서야 후기를 쓰게 되네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;관련 글&lt;/p&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.benelog.net/1230429&quot;&gt;QB로 만든 Code can be an art 출력&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.benelog.net/1230312&quot;&gt;코드와 예술&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저는 이 행사를 &lt;a href=&quot;http://www.goodhyun.com/&quot;&gt;김국헌님의 블로그&lt;/a&gt;에서 알게되어 혼자 행사장을 찾아갔었습니다. 제가 워낙 길치라서 노트북에다 약도를 저장해서 띄워놓고 화면을 계속 보고 갔는데도  힘들게 행사장을 찾아갔습니다. -_- 길에서 걷는 중에 노트북을 펴놓고 화면과 주위를 번갈아 가는 모습을 이상하게 본 사람도 있었을 것 같습니다.^^;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;주요 행사 내용 중에 다음과 같은 내용들이 인상깊었었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;dj성우님의_발표&quot;&gt;DJ성우님의 발표&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개발자와 DJ의 공통점으로 &apos;밤늦게 컴퓨터 앞에서 작업하는 일이 많다&apos;,&apos;이미 있는 것으로부터 새로운 창조물을 만든다&apos; 등을 드신 것이 기억납니다. 그리고 개발자들도 DJ들처럼 공동작업 그룹 같은것을 자발적으로 결성해서 활동하면 어떨까 하는 제안도 신선하게 들렸습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;미디어 아티스트  &lt;a href=&quot;http://beingmedia.tistory.com/entry/%EC%9E%85%EC%9E%90%EC%9D%B8%EC%83%9DNet&quot;&gt;최승준&lt;/a&gt;님의 발표&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;최승준 님의 &apos;입자인생&apos;(&lt;a href=&quot;http://beingmedia.tistory.com/entry/%EC%9E%85%EC%9E%90%EC%9D%B8%EC%83%9DNet&quot;&gt;http://beingmedia.tistory.com/entry/입자인생Net&lt;/a&gt; )이라는 작품은 예술적이면서 철학적인 설명을 해주셔서 심오하게 까지 느껴졌습니다. 그런 것 프로그래밍 하려면 물리학이나 수학도 많이 알아야 될 것 같다고 생각하니 저런 작품들은 여러 분야들의 종합성과물이라는 생각이 들었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;황리건님의_발표&quot;&gt;&lt;a href=&quot;http://reagan.egloos.com/226206&quot;&gt;황리건&lt;/a&gt;님의 발표&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;황리건님의 작품 중에는 맨 마지막 것이 가장 기억에 남습니다. 화면에 보이는 선의 한 부분을 클릭하면 거기서 원이 추가 되고 선분은 나누어져서 새로운 움직임을 보여주는 프로그램이였습니다. 이 작품을 꼭 보여주시고 싶어 하시면서, 이 것을 한달동안 힘들게 만드셨다고 말씀하시는데, 창작자로써의 뿌듯함이 느껴지시는 것 같았습니다. 요즘은 고객이 만들라고만 한 프로그램만 만들고 있는 저로서는 부러웠어요 ^^;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;코드잼&quot;&gt;코드잼&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.benelog.net/1230429&quot;&gt;QB로 만든 Code can be an art 출력&lt;/a&gt; 글에서 썼듯이 코드만 짜가고 발표는 못했습니다. 그래도 기획하신 분의 의도, 발표하신 분들의 기분은 알 수 있을 것 같았습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;패널토론_8bit_키드의_생애&quot;&gt;패널토론  8Bit 키드의 생애&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;80년대 초반 컴퓨터를 처음 시작한 세대들의 추억담이였습니다. 저보다는 앞세대의 분들이였고 사용한 기계와 환경은 달랐지만, 그런 이야기를 하시면서 어떤 기분이 드실지는 왠지 알 것 같았습니다. 아마 제가 &apos;허큘리스&amp;#8217;라는 말만 들어도  느껴지는 기분과 비슷하지 않을까요? ^^;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;패널토론_예술로서의_코드&quot;&gt;패널토론  예술로서의 코드&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;타이틀이 맞는지 모르겠군요. 김창준님이 귀여니의 시구를 인용하시던 부분에서 웃지 않을수 없었습니다. 앞의 다른 순서들이 &apos;코드로 만든 결과물인 예술&apos;, &apos;만든다는 것의 기쁨&amp;#8217;에 초점이 맞춰진 것이라면 이 순서는 &apos;코드 자체로써의 예술&amp;#8217;에 중심을 둔 것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모든 순서가 끝난 뒤에 뒤풀이가 있었습니다. 저는 아는 사람도 없고 해서 분위기가 어색하지 않을까 걱정을 했었는데, 저녁 식사 때부터 같은 테이블에 앉았던 자원봉사자 분들이 재밌는 자리를 만들어주셔서 편하게 술자리를 할 수 있었습니다 ^^; 그리고 개인적으로 평소 애독하는 블로그의 주인이신 류한석님, 김국헌님을 가까이서 뵐 수 있어서 영광이였습니다~ ^^ 같은 테이블에 앉았던  대학생 분은 그 두분께  블로그 내용이 인쇄된 종이에다가 사인을 해달라고  부탁하더군요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;보통 저는 술자리에 참석하면 끝까지 남거나 쓰러지거나 둘 중에 하나였었는데, 이날은 아내가 근처까지 데리러 오는 바람에 먼저 일어날수 밖에 없어서 무척 아쉬웠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다음 모임때부터는 저도 자원봉사자로 참여하겠다고 했는데 빨리 그 때가 왔으면 좋겠네요~&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>QB로 만든 Code can be an art 출력</title>
      <link>https://blog.benelog.net//1230429.html</link>
      <pubDate>Fri, 1 Jun 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">1230429.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.creativecommons.or.kr/&quot;&gt;CCK(Creative Commons Korea)&lt;/a&gt;의 Code can be an art 행사에서는 &apos;코드잼&amp;#8217;이라는 순서가 있었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&apos;Code can be an art&amp;#8217;라는 문자열을 출력하는 프로그램을 자유롭게 짜서 발표하는 것이었죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;코드잼 설명 :  &lt;a href=&quot;http://ccsalon.tistory.com/4&quot; class=&quot;bare&quot;&gt;http://ccsalon.tistory.com/4&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저도 이날 점심을 먹는 것도 잊고 나름대로 코드를 준비해 갔는데, 발표를 하지는 못했네요 .  발표자가 적은 분위기면 앞에 나가볼려고 노트북까지 들고 가기는 했었습니다 ^^; 그런데 열띤 호응으로  많은 분들이 발표를 하셨고, MSX 에뮬레이터로 하신 분 등 제가 말하고 싶었던 내용을 다른 분들이 더 훌륭히 말씀해 주셔서 큰 아쉬움은 없습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;8bit 화면 같은 행사 로고를 보고 저도  제 세대에 맞게 Quick Basic으로 코드를 짜볼 생각을 해봤습니다. 이 행사 중에 &apos;8bit kid의 생애&amp;#8217;라는 패널토론 주제도 있었었죠. ^^; 저는 그 다음 시대인 IBM XT세대입니다. 허큘리스 그래픽카드, 5.25인치 FDD, 조금 지나서 ADLIB card, 2400bps MNP 모뎀 등이 저에게는 가슴을 파고드는 핵심단어들이에요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그 추억의 시절에 제가 가장 많아 가지고 놀았던 프로그램 언어가 Quick-Basic이였습니다. Basic에서 행번호를 없애고 구조화프로그래밍 개념을 도입한, 당시 저에게는 충격적으로 대단한 언어였어요.   이 도구에다  basic을 배웠던 사람이면 누구나 회상할 추억의 명령어 line(선긋기), play(음악연주), read,data(데이터 읽는 구문)을 넣어서 코딩을 해봐야겠다고 마음 먹었습니다. 대신 제 PC환경상 그때의 Quick-basic 대신 Window XP에 내장되어 있는 q-basic을 사용할 수 밖에 없었어요. 프롬프트에서 qbasic이라고 치면 언제나 튀어나오는 도구죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Quick-Basic과 q-basic이 뭐가 다르냐구요? 문법은 거의 똑같은것 같아요. Quick-basic은 따로 패키지로 나와서 버전4.5까지 발표되었었는데, q-basic은 어느 새 MS-DOS의 상위버전에 포함되어서 나오더군요 . Quick-Basic에서는 실행파일(.exe)도 만들 수 있고, 각종 라이브러리도 많았었습니다. 아뭏든 대타로 q-basic을 썼기에 실행할때는  아래와 같이 치면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;quoteblock&quot;&gt;
&lt;blockquote&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;프롬프트&amp;gt; qbasic/run cba2.bas&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;단순히 화면에 큰 글자를 찍어주는 것 밖에 없지만, 꽤나 시행착오 작업을 많이 거쳤었습니다. 예전 Quick-Basic에서는 screen 12라는 화면모드를 주로 썼었었는데, 이번 환경에서는 screen 2 밖에 안 먹는 것이였습니다. 화면 전체 해상도가 몇곱하기 몇 인지도 모르고 그냥 무작정 네모를 그리는 숫자값을 바꿔가면서 화면을 만들어 봤습니다. 결국 한참 후에야 그럭저럭 화면중간에 크게 들어간 글자를 얻어낼 수가 있었죠. 평소에 일할 때 이렇게 무식하게 작업을 한다면 스스로 한심하게 느껴졌겠지만 이번에는 그것조차도 옛 추억을 되살리는 즐거움이였습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고 이 프로그램에 들어간 멜로디를 아시는 분이라면 아마 저하고 비슷한 컴퓨터 세대이실 것 같아요. 불후의 고전명작게임 &apos;Loom&amp;#8217;의 메인테마 입니다. 처음에 노트북을 들고 행사장에 갈때까지만 해도 프로그램에서는 단순히 &apos;도미솔&apos; 정도의 소리만 났었습니다. 그러다가 행사가 끝난 다음 주에 문득 여기에다 듣기좋은 멜로디를 넣어보고 싶다는 충동을 느꼈고, 그때 게임 &apos;Loom&amp;#8217;의 음악이 머리 속에 떠올랐습니다. 당연히 악보 같은 건 없었고 기억하는 멜로디를 스스로 휘파람으로 불어서 청음으로 코드를 만들었습니다. 남다른 음감각을 가진 것도 아니어서 계속 시행착오를 거치면서  play문에 들어가는 음표코드를 넣었죠. 어쩐지 박자와 음이 약간 어설프고 불안정한 느낌이 들지 않나요?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;행사 때 김국헌님께서 발표하시는 분들마다 물어보셨던 말이 있었습니다. &apos;이 작업을 하시면서 즐거웠습니까?&apos;&apos; 저도 여기에 그때 다른 분들이 하셨던 말과 똑같이 &apos;네. 굉장히 즐거웠습니다&amp;#8217;라고 말할 수 밖에 없네요. 한 때는 왠만한 단축키는 다 외웠었던 Quick-Basic이였는데 오랜만에 쓸려니 함수 선언방법 조차도 가물가물 하더군요. 그래도 기억을 되살려 가면서 작업을 하다보니 이런 도구로 고등학교 때 축제 전시회 때 내놓을 게임을 만들던 일이 생각이 났었습니다. 그 때 정말 밥 먹는 시간도 잊어 버릴 정도로 매달려서 어설픈 게임을 만들었고, 어쩌면, 아니 확실히 그런 기억들 때문에 지금도 컴퓨터 코드를 짜는 일을 하고 있다는 사실도 새삼스럽게 떠올랐습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;뭐 어떤 분들은 이 글을 보시면, 몇줄 안되고 실행결과도 허접한 프로그램 짜놓고 제목은 아트니 뭐니 되어 있고 되게 거창한 척을 한다고 느끼실 것 같습니다. 하지만 누가 뭐래든 이 코드를 짜던 순간, 그리고 Quick-Basic이 어떤거니 하는 , 요즘은 별 정보도 되지 않을 이런 글을 주절주절 쓰고 있는 이 순간까지도 행복감의 여운을 느낍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;다 해놓고 보니 코드는 그리 길지는 않네요~&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;basic&quot;&gt;DECLARE SUB display (c$, p$)

SCREEN 2
lg$ = &quot; Code can be an art! &quot;
DATA &quot;t90O1p4&quot;,&quot;e8&quot;,&quot;a8&quot;,&quot;b8&quot;,&quot;&amp;gt;c+8&quot;,&quot;g8&quot;,&quot;f8&quot;,&quot;&amp;lt;a8&quot;
DATA &quot;&amp;gt;d8&quot;,&quot;c+8&quot;,&quot;&amp;lt;e2&quot;,&quot;e8&quot;,&quot;a4&quot;,&quot;b8&quot;,&quot;&amp;gt;c+4&quot;,&quot;&amp;lt;b8&quot;,&quot;a2&quot;,&quot;e2&quot;,&quot;p4&quot;,&quot;p4&quot;,&quot;p4&quot;,&quot;p4&quot;
FOR i = 1 TO LEN(lg$)
    READ p$
    display MID$(lg$, i, 1), p$
NEXT i
SCREEN 0
END

SUB display (c$, p$)
PLAY p$
CLS
PRINT c$
FOR i = 0 TO 10
  FOR j = 0 TO 10
    IF POINT(j, i) THEN LINE (j * 60 + 80, i * 25 + 10)-(j * 60 + 140, i * 25 + 40), 1, BF
  NEXT j
NEXT i
LOCATE 1, 1
PRINT &quot; &quot;

END SUB&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;다른분들의_발표작&quot;&gt;다른분들의 발표작&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;http://colus.egloos.com/3421372&quot; class=&quot;bare&quot;&gt;http://colus.egloos.com/3421372&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sqler.com/board_uxkorea/172190&quot; class=&quot;bare&quot;&gt;https://www.sqler.com/board_uxkorea/172190&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>방명록</title>
      <link>https://blog.benelog.net//904735.html</link>
      <pubDate>Thu, 8 Feb 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">904735.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;블로그 방명록이에요. 댓글로 달아주세요&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>나선형 배열 출력 문제 Spiral Array</title>
      <link>https://blog.benelog.net//901106.html</link>
      <pubDate>Wed, 7 Feb 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">901106.html</guid>
      	<description>
	&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;2007년 회사 워크샵 갔었을 때 풀었던 문제와 답&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;문제&quot;&gt;문제&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;img/quiz/spiral-array.gif&quot; alt=&quot;image&quot; width=&quot;480&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;풀이&quot;&gt;풀이&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;정상혁&amp;amp;이상미 조의 풀이 답입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;public class MatrixMain {
    public static void main(String[] args) {
       Matrix mp = new Matrix(5,5);
       mp.locateNumbers();
       mp.print();
    }
}

public class Matrix {

    public static final int RIGHT = 0;
    public static final int DOWN = 1;
    public static final int LEFT = 2;
    public static final int UP  = 3;

    private static final int INIT_VALUE = -1;

    private int m = 0;
    private int n = 0;
    private int[][] matrix;

    public Matrix(int m, int n){
        this.m = m;
        this.n = n;
        matrix = new int[m][n];
    }

    public void print(){
        for (int i=0;i&amp;lt; m ;i++){
            for (int j=0;j&amp;lt;n;j++) System.out.print(matrix[i][j] + &quot;\t&quot;);
            System.out.println();
       }
    }

    public void locateNumbers() {
       init();
       int[] startPosition = new int[]{0,0};
       matrix[0][0]= 0;
       int direction = RIGHT;
       while(move(startPosition ,direction)) direction ++;
    }

    private void init() {
        for (int i=0;i&amp;lt; m ;i++){
            for (int j=0;j&amp;lt;n;j++) matrix[i][j] = INIT_VALUE;
        }
    }

    private boolean move(int[] position, int direction){
        int nowNumber = matrix[position[0]][position[1]];
        boolean moved = false;
        int[] nextPosition =getNextPosition(position,direction);
        while(isMovable(nextPosition[0],nextPosition[1])){
            moved = true;
            nowNumber++;
            position[0] = nextPosition[0];
            position[1] = nextPosition[1];
            matrix[position[0]][position[1]]= nowNumber;
            nextPosition = getNextPosition(position,direction);
        }
        return moved;
    }

    private int[] getNextPosition(int[] position,int direction){
        int x = position[0];
        int y = position[1];
        int nowDirection = direction%4;
        if (nowDirection == RIGHT) y++;
        else if (nowDirection == DOWN) x++;
        else if (nowDirection == LEFT) y--;
        else if (nowDirection == UP) x--;
        return new int[]{x,y};
    }

    private boolean isMovable(int x, int y){
        if (x&amp;gt;=m) return false;
        if (y&amp;gt;=n) return false;
        if (x&amp;lt;0) return false;
        if (y&amp;lt;0) return false;
        if (matrix[x][y]!= INIT_VALUE) return false;
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;결과&quot;&gt;결과&lt;/h3&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;prettyprint highlight&quot;&gt;&lt;code&gt;0   1   2   3   4

15  16  17  18  5

14  23  24  19  6

13  22  21  20  7

12  11  10  9   8&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이동하는 좌표를 사이즈가 2인 int배열에다 담았는데, 이걸 따로 클래스로 빼서 만들까 하다가 클래스를 더 늘리기 싫어서 그만뒀습니다.
소스에서 &lt;code&gt;position[0], position[1]&lt;/code&gt; 이라고 표시된 부분이 좌표를 나타내는 부분입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;main메서드에서  &lt;code&gt;Matrix mp = new Matrix(5,5);`&lt;/code&gt; 부분이 5*5 행렬을 만든다는 의미입니다.
사이즈를 바꿀려면 이 부분을 고치시거나, 사용자 입력에서 온 값을 연결시키면 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
	</description>
    </item>
    <item>
      <title>더골(THE GOAL)</title>
      <link>https://blog.benelog.net//900996.html</link>
      <pubDate>Wed, 7 Feb 2007 00:00:00 +0000</pubDate>
      <guid isPermaLink="false">900996.html</guid>
      	<description>
	&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=244882&amp;amp;CategoryNumber=001001025008009&quot;&gt;&lt;span class=&quot;image&quot;&gt;&lt;img src=&quot;http://image.yes24.com/momo/TopCate04/MidCate10/391510.jpg&quot; alt=&quot;image&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;(이미지는 yes24에서)&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;더 골(The Goal)  , 제프 콕스 저, 김일운 역, 동양문고&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;회사 소모임의 워크샵에서 이 책을 주제로 한 발표가 있었습니다. 그 때 생각난 내용을 나중에 회사 커뮤니티 게시판에 적었는데, 그 내용의 일부를 옮겨써 봅니다.&lt;/p&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;저의 사촌형이 이 책을 낸 출판사에 일하고 있어서, 저는 책을 공짜로 받았었습니다. 몇년전 제가 그 형 집에 놀러 갔을때 이책을 아마 제가 꼭 읽어야 할 것이라면서 그냥 저에게 주는 것이였습니다. 대충 넘겨보면서 내용을 훑어보니 공장에 대한 이야기라서 서비스업에 종사하는 저로써는 별 상관없는 내용으로만 보였습니다. 그래서 한동안 책장에 꽂아만 두고 있었죠.  책을 받고 한참 뒤에서야 잡지나 블로그 등을 통해 개발자가 읽을 책으로  이 책을 권하는 사람이 많은 것을 알게 되었고,  다시 찾아서 읽게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;소설의 내용은 공장장이 문제의 공장을 해결해 나가는 이야기입니다.  그 문제의 공장이 잘 돌아가지 않았던 것은  각각의 공정의 &lt;strong&gt;부분적인&lt;/strong&gt; 생산성에만 작업자의 목표가 맞춰져 있었기 때문이라고 정리하고 싶습니다. &quot;부분적인 최적화는 전체 최적화를 보장하지 않는다&quot;는 말도 학교 다닐때 들었는데, 그 예가 이 소설 내용에 있는 것 같네요.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;공장에서는 자동로봇을 도입하고 이를 자랑꺼리로 여겼는데, 그 로봇은 많은 부품을 찍어냈지만, 그 부품은 결국 재고만 더 쌓이게 만들어서 오히려 공장에 손해만 끼치는 결과를 낳았다는 내용이 있습니다. 그 로봇이 있는 공정에서는 부품의 생산양만 보고 높은 성과를 이루고 있다고 생각했겠죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;모두들 열심히 일하고 로봇도 쉬지 않고 있는 공장이 고객이 주문하는 생산량을 못 맞추자 공장장은 대학 때 교수로 부터 자문을 받아서 이를 해결해 나가기 시작합니다. 그 방법들은&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;병목이 일어나는 공정에 대해서 외주를 주거나 동원가능한 다른 기계를 이용해서 지원작업을 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;우선 순위가 급한 부품을 다른 표시를 해서 그 부품이 해당 공정에 들어왔을때는 가장 먼저 작업을 하도록 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;등입니다. 위에 로봇의 경우와는 반대로 최적화된 한번의 작업량 (예를 들면 붕어빵틀에 모든 구멍에 밀가루를 채우는 것 같이 해당공정의 효율이 극대화 되는 한번의 양)을 포기했을 때 오히려 공장의 전체 생산성이 더 좋아지는 예도 책에 나옵니다. 그리고 &quot;재고는 자산이 아닌 비용이다&quot;라는 개념도 기존 통념을 뒤엎는 것이였죠.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;전에 이 책을 다 읽고나서도 &quot;그래도 나와는 큰 관련 없는 공장이야기군&quot;하고 생각을 했었는데,  최근에 들어서야 IT업과 연관성을 생각해보게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;개발 프로젝트를 하면서 제가 경험한 일정관리는 프로그램의 목록을 엑셀시트나 이슈 트래커에다 나열해 놓고 그중에 몇개를 완성했느냐, 그것이 전체의 몇%냐로 평가를 받는 것이였습니다. 그 갯수가 적으면 다른 사람보다 생산성이 적은 것이 되고, 계획했던 갯수보다 적으면 일정을 못 맞춘것이 되는것이였습니다. 여기서 개발자가 충족해야할 목표는 그 갯수를 늘이는 것인데, 그러다보니 쉬운 프로그램, 고객이 별로 신경쓰지 않는 프로그램을 먼저 구현을 하게 되는 경향이 생기기 쉬워집니다. 그러다보면 오히려 정말 많이쓰이고, 고객이 민감해 하는 프로그램들은 나중에 급하게 졸속으로 구현되는 경우를 봐왔습니다. 이는 개발자가 평가받는 부분적인 목표(프로그램 갯수)와 프로젝트 성공의 목표(고객의 필요한 프로그램이 잘 돌아가서 검수를 받는것)이 상충되는것이 아닐까 합니다. 익스트림 프로그래밍 서적에서도 비슷한 이야기를 본것 같네요. 고객이 가장 우선시 하는것부터 먼저 구현을 하고 고객에게 피드백을 받고 다음 스토리로 넘어가야한다는 내용이였던것 같습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;그리고  프로젝트에서 &quot;고객검토&quot;가 병목인 경우가 많다는 생각도 듭니다. 한참동안 피드백이 없다가 프로젝트 종료시점에서야 요구사항을 쏟아내는 고객의 경험을 다른 분들도 겪었을것 같습니다. 병목을 보강하듯이 업무별 고객담당자를 적절히 분할해서 지정하거나, 개발팀 쪽에서도 고객검토을 촉진시킬수 있는 담당자를 두거나 하는 해결방안등이 생각이 납니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;이 책이 공장의 이야기인데도 개발자들사이에 많이 권해지는 이유를 연결시켜서 생각해봤습니다. 그 외에도 연관시킬만한  것이 있는지 궁금합니다.&lt;/p&gt;
&lt;/div&gt;
	</description>
    </item>

  </channel> 
</rss>
