<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>중꺾마</title>
    <link>https://okimaru.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 23 May 2026 09:56:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Z_Z</managingEditor>
    <image>
      <title>중꺾마</title>
      <url>https://tistory1.daumcdn.net/tistory/4920808/attach/2b80a4da6e464841ac074c0e2c1f5ebb</url>
      <link>https://okimaru.tistory.com</link>
    </image>
    <item>
      <title>HttpSessionEventPublisher</title>
      <link>https://okimaru.tistory.com/entry/HttpSessionEventPublisher</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HttpSessionEventPublisher&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 에서 사용하는 클래스인데, 서블릿 컨테이너에서 발생하는 HttpSession 이벤트를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 쪽 이벤트로 변환해서 전달해주는 다리 역할을 한다. 즉, 톰캣 같은 WAS 가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 생성 / 삭제 / ID 변경을 알리면, 그걸 Spring 어플리케이션 컨텍스트가 들을 수 있는 이벤트로 변환하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 쉽게 말하면&lt;/p&gt;
&lt;pre id=&quot;code_1779148370370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Tomcat 세션 이벤트
   &amp;darr;
HttpSessionEventPublisher
   &amp;darr;
Spring Security 이벤트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;왜 필요한가&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 세션 관련 기능, 특히 동시 세션 제어나 세션 만료 감지는 컨테이너의 HttpSessionListener 이벤트만으로는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족하다. HttpSessionEventPublisher 가 있어야 세션 생성/종료 시점에 Spring 쪽 SessionDestroyedEvent 같은 이벤트가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 올라온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS는 세션이 생성/삭제될 때 아래 이벤트를 발생시킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1779148542642&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HttpSessionListener&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;하지만 Spring Security 는 이 이벤트를 직접 받지 못한다.&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;그래서 아래의 HttpSessionEventPublisher 가 대신 받아서 Spring 이벤트로 변환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779148627647&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HttpSessionEventPublisher&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부 동작 흐름&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 세션 만료 시 아래와 같이 동작하는 구조이다.&lt;/p&gt;
&lt;pre id=&quot;code_1779148812369&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. Tomcat 이 세션 제거
2. HttpSessionListener 호출
3. HttpSessionEventPublisher 실행
4. Spring ApplicationEvent 발행
5. SessionDestroyedEvent 발생
6. ApplicationListener&amp;lt;SessionDestroyedEvent&amp;gt; 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 클래스 구조&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1779149035864&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class HttpSessionEventPublisher
        implements HttpSessionListener, HttpSessionIdListener&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;등록 방법(Spring Boot 3)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1779149124384&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class SessionConfig {

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
    
    @Bean
	ServletListenerRegistrationBean&amp;lt;HttpSessionEventPublisher&amp;gt; getHttpSessionEventPublisher() {
	    return new ServletListenerRegistrationBean&amp;lt;HttpSessionEventPublisher&amp;gt;(httpSessionEventPublisher());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <category>httpsessioneventpublisher</category>
      <category>Spring</category>
      <category>Spring boot</category>
      <category>spring security</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/485</guid>
      <comments>https://okimaru.tistory.com/entry/HttpSessionEventPublisher#entry485comment</comments>
      <pubDate>Tue, 19 May 2026 09:00:52 +0900</pubDate>
    </item>
    <item>
      <title>ApplicationListener&amp;lt;SessionDestroyedEvent&amp;gt;</title>
      <link>https://okimaru.tistory.com/entry/ApplicationListenerSessionDestroyedEvent-HttpSessionEventPublisher</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ApplicationListener&amp;lt;SessionDestroyedEvent&amp;gt; 란?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 에서 세션(Session) 이 종료되거나 만료될 때 발생하는 이벤트를 감지하는 리스너이다.&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;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;Cocurrent Session Control 로 인한 세션 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SessionDestroyedEvent&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SessionDestroyedEvent 는 단순히 브라우저 탭을 닫는 순간이 아니라, Spring Security 가 관리하는 세션이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만료되거나 무효화될 때 발생하는 이벤트입니다. 이 이벤트 안에는 보통 해당 세션에 연결된 SecurityContext 목록이&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통 같이 필요한 것(HttpSessionEventPublisher)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 리스너만 등록해서는 부족하고, HttpSessionEventPublisher 도 함께 등록해야 세션 생성/삭제 이벤트가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 이벤트로 전달됩니다. Spring Boot 에서는 보통 설정 클래스에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServletListenerRegistrationBean&amp;lt;HttpSessionEventPublisher&amp;gt; 형태로 빈 등록을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779146988933&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

@Bean
ServletListenerRegistrationBean&amp;lt;HttpSessionEventPublisher&amp;gt; getHttpSessionEventPublisher() {
    return new ServletListenerRegistrationBean&amp;lt;HttpSessionEventPublisher&amp;gt;(httpSessionEventPublisher());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;왜 필요하냐?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 는 기본적으로 WAS 의 세션 이벤트를 직접 받지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 구조처럼 WAS 의 이벤트를 HttpSessionEventPublisher 리스너가 대신 받아 Spring Event 로 변환하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SessionDestroyedEvent 를 발생시킨다.&lt;/p&gt;
&lt;pre id=&quot;code_1779147471004&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WAS Session Event
   &amp;darr;
HttpSessionEventPublisher
   &amp;darr;
Spring Event 로 변환
   &amp;darr;
SessionDestroyedEvent 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단 예시&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SessionDestroyedEvent 는 Spring Security 가 발행하는 이벤트이다.&lt;/p&gt;
&lt;pre id=&quot;code_1779147138878&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class SessionDestroyedListener 
        implements ApplicationListener&amp;lt;SessionDestroyedEvent&amp;gt; {

    @Override
    public void onApplicationEvent(SessionDestroyedEvent event) {

        System.out.println(&quot;세션 종료됨&quot;);

        event.getSecurityContexts().forEach(context -&amp;gt; {
            String username = context.getAuthentication().getName();

            System.out.println(&quot;로그아웃 사용자 : &quot; + username);
        });
    }
}&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;</description>
      <category>Java/Spring</category>
      <category>httpsessioneventpublisher</category>
      <category>SessionDestroyedEvent</category>
      <category>Spring</category>
      <category>spring security</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/484</guid>
      <comments>https://okimaru.tistory.com/entry/ApplicationListenerSessionDestroyedEvent-HttpSessionEventPublisher#entry484comment</comments>
      <pubDate>Tue, 19 May 2026 08:46:15 +0900</pubDate>
    </item>
    <item>
      <title>OpenCode 란?</title>
      <link>https://okimaru.tistory.com/entry/OpenCode-%EB%9E%80</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;OpenCode 란?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널이나 IDE 안에서 동작하는 오픈소스 AI 코딩 에이전트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람의 언어인 자연어로 명령을 해서 코드를 생성, 수정, 분석하는 오픈소스 AI 코딩 에이전트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 자동완성 수준이 아니라 프로젝트 전체를 읽고 코드 수정, 파일생성, 테스트 실행까지 해주는 개발용 AI 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;핵심기능&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자연어 기반 코드 작업(생성, 리팩토링, 버그 수정 등)을 터미널 또는 IDE에서 실행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;여러 LLM(예: OpenAI, Anthropic/Claude, Google Gemini 등)을 API 키로 연결해 사용자가 모델 선택을 유연하게 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아키텍처ㆍ확장성&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CLI 중심으로 동작하지만 플러그인(또는 하네스)을 통해 서브에이전트 ㆍ도구(브라우저 자동화, Git 연동, 테스트 실행 등)를 추가할 수 있어 확장이 쉽다.&lt;/li&gt;
&lt;li&gt;설정 파일(프로젝트/사용자 레벨)을 통해 에이전트, 모델, 온도 등 실행 파라미터를 세부적으로 구성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; GitHub Copilot이랑 차이 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 항목 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; OpenCode &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; GitHub Copilot &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;AI 에이전트&lt;/td&gt;
&lt;td&gt;자동완성 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;동작&lt;/b&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;b&gt;터미널 작업&lt;/b&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;b&gt;모델 선택&lt;/b&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;b&gt;오픈소스&lt;/b&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;b&gt;프로젝트 분석&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉,&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Copilot &amp;rarr; &amp;ldquo;코드 추천&amp;rdquo;&lt;/li&gt;
&lt;li&gt;OpenCode &amp;rarr; &amp;ldquo;개발 작업 수행&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 실제 개발에서 많이 쓰는 기능 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 버그 수정&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NullPointerException&amp;nbsp;원인&amp;nbsp;찾아서&amp;nbsp;수정해줘&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 리팩토링 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Service 계층 구조 정리해줘&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 장점 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무료/오픈소스 기반&lt;/li&gt;
&lt;li&gt;터미널 친화적&lt;/li&gt;
&lt;li&gt;여러 AI 모델 사용 가능&lt;/li&gt;
&lt;li&gt;로컬 모델 연동 가능&lt;/li&gt;
&lt;li&gt;대형 프로젝트 분석 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영어 기반 프롬프트가 아직 더 강함&lt;/li&gt;
&lt;li&gt;AI 가 잘못 수정할 수도 있음&lt;/li&gt;
&lt;li&gt;토큰 비용 발생 가능(OpenAI / Claude API 사용 시)&lt;/li&gt;
&lt;li&gt;초보자에겐 설정이 약간 복잡할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI</category>
      <category>AI</category>
      <category>opencode</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/482</guid>
      <comments>https://okimaru.tistory.com/entry/OpenCode-%EB%9E%80#entry482comment</comments>
      <pubDate>Fri, 15 May 2026 10:35:59 +0900</pubDate>
    </item>
    <item>
      <title>Spring WebMvcConfigurer</title>
      <link>https://okimaru.tistory.com/entry/Spring-WebMvcConfigurer</link>
      <description>&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;Spring MVC 기본 동작을 갈아엎지 않고 필요한 부분만 커스터마이징하는 인터페이스이다&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅ 1. 어디에 쓰는가?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 는 기본적으로 내부에서 MVC 설정을 자동으로 해준다&lt;br /&gt;WebMvcConfigurer 인터페이스를 구현하면 DispatcherServlet 과 관련된 MVC 설정(인터셉터, CORS, 리소스 핸들링, 뷰 설정 등) 을 Java 기반으로 커스텀할 수 있다&lt;br /&gt;&lt;br /&gt;여기서 기본 설정은 그대로 두고 필요한 부분만 커스터마이징하기 때문에 전체 설정을 다시 만드는것보다 간편하다&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 설정하게 되면 Spring MVC 설정에 끼어들 수 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1️⃣ 정적 리소스 매핑&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;registry.addResourceHandler(&quot;/wvoice/**&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.addResourceLocations(&quot;file:/data/tts/&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;/wvoice/ 이후의 경로를 resourceLocation 경로 뒤에 매핑시켜 실제 파일 경로로 접근하여 처리할 수 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2️⃣ 인터셉터 등록&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Override
public void addInterceptors(InterceptorRegistry registry) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;registry.addInterceptor(new LoginInterceptor())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.addPathPatterns(&quot;/**&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.excludePathPatterns(&quot;/login&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;필터 이후에 접근한 요청에 대해 URL 로 구분하여 처리할 수 있다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3️⃣ CORS 설정&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Override
public void addCorsMappings(CorsRegistry registry) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;registry.addMapping(&quot;/**&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.allowedOrigins(&quot;*&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.allowedMethods(&quot;*&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;요청 도메인과 접근한 도메인이 다를때 처리한다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4️⃣ ViewResolver 설정&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;registry.jsp(&quot;/WEB-INF/views/&quot;, &quot;.jsp&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;JSP 같은 뷰 설정을 한다&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5️⃣ 메시지 컨버터 (JSON 등)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Override
public void extendMessageConverters(List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; converters) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Object -&amp;gt; Json 변환 커스터마이징&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6️⃣ 포맷터 / 컨버터&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;Java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;@Override
public void addFormatters(FormatterRegistry registry) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;날짜, enum 변환 등&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <category>Spring</category>
      <category>webmvcconfigurer</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/481</guid>
      <comments>https://okimaru.tistory.com/entry/Spring-WebMvcConfigurer#entry481comment</comments>
      <pubDate>Thu, 23 Apr 2026 13:57:28 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot WebMvcConfigurer addResourceHandlers 설정</title>
      <link>https://okimaru.tistory.com/entry/Spring-Boot-WebMvcConfigurer-addResourceHandlers-%EC%84%A4%EC%A0%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 에서 특정 파일을 /static 리소스 경로에 생성하고 그 파일을 브라우저의 Audio 기능을 통해 음성파일을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행했어야 했는데 파일을 생성한 후 실행하려고 보니까 에러가 났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러의 원인은 파일 리소스를 어플리케이션에서 reload 를 해주기 직전에는 리소스를 인식하지 못하는 이유였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 원인을 해결하기 위해 특정 파일 경로를 설정하고 특정 요청으로 왔을 때 파일이 있는 실제&lt;/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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WebMvcConfigurer 설정&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1776917607783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@Profile(&quot;!test&quot;)
public class WebMvcConfig implements WebMvcConfigurer {
	@Value(&quot;${urgent.preview.upload.use}&quot;)
	private boolean URGENT_PREVIEW_UPLOAD;		// true
	
	@Value(&quot;${urgent.preview.tts.file.upload.path}&quot;)
	private String URGENT_PREFIEW_FILE_UPLOAD_PATH ; 		// c:/data/upload/tts
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    	if(URGENT_PREVIEW_UPLOAD) {
    		String urgentLocation = Paths.get(URGENT_PREFIEW_FILE_UPLOAD_PATH)
                    .toAbsolutePath()
                    .normalize()
                    .toUri()
                    .toString();
    		
    		if (!urgentLocation.endsWith(&quot;/&quot;)) {
                urgentLocation += &quot;/&quot;;
            }
    		
    		registry.addResourceHandler(&quot;/static/js/tts/**&quot;)
			            .addResourceLocations(urgentLocation)
			            .setCachePeriod(0)
			            .resourceChain(true)
			            .addResolver(new PathResourceResolver());

    		log.info(&quot;add urgent preview tts path: {}&quot;, urgentLocation);
    	}
    }
}

// properties 데이터
urgent.preview.tts.file.upload.path=c:/data/upload/tts&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;addResourceHandler(&quot;/static/js/tts/**&quot;)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본인이 음성 파일 실행을 위해서 아래와 같이 javascript 실행요청을 하게 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;const audio = new Audio(`${contextPath}/static/js/tts/${res.data.wav_file_name}?v=${Date.now()}`);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Handler 에 &quot;/static/js/tts/&quot; 설정 이후 **&amp;nbsp; 음성파일명만 .addResourceLocation 에 설정된 URL 로 매핑된다.&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;color: #000000;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 73px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 22px;&quot;&gt;&lt;b&gt; 요청 URL &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 22px;&quot;&gt;&lt;b&gt; 실제 찾는 파일 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;&lt;b&gt;/static/js/tts/EmergencyA.wav&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;C:/data/upload/tts/EmergencyA.wav &lt;span&gt;&lt;/span&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;baeldung.xiaocaicai&lt;/span&gt;&lt;span&gt;&lt;span&gt;+1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;&lt;b&gt;/static/js/tts/EmergencyA.wav&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;C:/data/upload/tts/js/tts/EmergencyA.wav&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;&lt;b&gt;/static/js/tts/a/b/c/test.wav&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;C:/data/upload/tts/a/b/c/test.wav &lt;span&gt;&lt;/span&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 ** 로 매핑된 문자열이 resourceLocation 설정 URL 뒤에 나머지 경로로 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   어떻게 동작하냐 (핵심) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;   실제 파일 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;C:/data/upload/tts/js/tts/EmergencyA.wav&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   설정 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ResourceHandler: /static/js/tts/**&lt;br /&gt;ResourceLocation: file:C:/data/upload/tts/&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   요청이 들어오면 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;/static/js/tts/EmergencyA.wav&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring이 이렇게 변환함: &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;file:C:/data/upload/tts/ + EmergencyA.wav&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   결과: &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;C:/data/upload/tts/js/tts/EmergencyA.wav&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;</description>
      <category>Java/Spring</category>
      <category>addResourceHandler</category>
      <category>addResourceLocation</category>
      <category>Spring</category>
      <category>webmvcconfigurer</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/480</guid>
      <comments>https://okimaru.tistory.com/entry/Spring-Boot-WebMvcConfigurer-addResourceHandlers-%EC%84%A4%EC%A0%95#entry480comment</comments>
      <pubDate>Thu, 23 Apr 2026 13:23:47 +0900</pubDate>
    </item>
    <item>
      <title>Javascript 접근성 경고(focus management)</title>
      <link>https://okimaru.tistory.com/entry/Javascript-%EC%A0%91%EA%B7%BC%EC%84%B1-%EA%B2%BD%EA%B3%A0focus-management</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;에러 내용은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Modal 창을 제어하다가 Modal 창을 닫을 때 숨겨질 영역안의 버튼이나 input에 포커스가 남아 있으면, 보조기술 입장에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포커스는 거기에 있는데 그 역역은 숨겨져 있는 모순 상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 본인은 Modal 창을 닫을 때 닫기 버튼에 대한 포커싱이 남아있어서 브라우저 콘솔창에 경고가 찍혔다.&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;원인은 Bootstrap 의 modal 속성에 aria-hidden=&quot;true&quot; 를 적용하자 브라우저가 &quot;포커스된 요소를 숨기면 안된다&quot; 고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고를 띄운것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1776399553987&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Blocked aria-hidden on an element because its descendant retained focus. 
The focus must not be hidden from assistive technology users. 
Avoid using aria-hidden on a focused element or its ancestor. 
Consider using the inert attribute instead, which will also prevent focus. For more details, 
see the aria-hidden section of the WAI-ARIA specification at [https://w3c.github.io/aria/#aria-hidden](https://w3c.github.io/aria/#aria-hidden).
Element with focus: &amp;lt;button.btn btn-default&amp;gt;
Ancestor with aria-hidden: 
&amp;lt;div.manager-dialog manager-site-dialog modal draggable fade in#urgentMessageServerDialog&amp;gt; &amp;lt;div class=​&quot;manager-dialog manager-site-dialog modal draggable fade in&quot; id=​&quot;urgentMessageServerDialog&quot; tabindex=​&quot;-1&quot; style=​&quot;display:​ none;​&quot; aria-hidden=​&quot;true&quot;&amp;gt;​&amp;hellip;​&amp;lt;/div&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;blur()&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;blur() 는 현재 포커스된 요소에서 포커스를 제거하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 모달을 닫기 전에 document.activeElement.blur() 를 해주면 닫는 순간 모달 안에 포커스가 남아 있지 않게 만들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경고를 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면:&lt;/p&gt;
&lt;pre id=&quot;code_1776399851999&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const activeEl = document.activeElement;
if (activeEl &amp;amp;&amp;amp; typeof activeEl.blur === 'function') {
    activeEl.blur();
}&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;Modal의 data-bs-dismiss 속성을 없애니 해결됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;data-bs-dismiss=&quot;modal&quot; 를 쓰면 Bootstrap 이 버튼 클릭 직후 자기 타이밍으로 바로 hide 를 처리한다.&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;이때 개발자가 blur() -&amp;gt; hide() 순서를 세밀하게 제어하기 어려워서, 닫기 버튼에 포커스가 남은 채 aria-hidden 이 먼저 적용되어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경고성 메시지가 뜬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 dismiss 뺴고 직접 닫으면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;버튼 포커스 해제&lt;/li&gt;
&lt;li&gt;필요시 opener(modal 창을 뜨게 만든 버튼) 포커스 이동&lt;/li&gt;
&lt;li&gt;modal.hide()&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 브라우저는 이걸 경고하나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키보드 사용자나 스크린리더 사용자는 현재 포커스가 어디 있는지가 매우 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 포커스가 숨겨진 요소에 남아 있으면:&lt;/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;탭 이동 흐름이 꼬이거나 예기치 않은 UX 가 생길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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>JavaScript/javascript</category>
      <category>blur()</category>
      <category>data-bs-dismiss</category>
      <category>document.activeElement</category>
      <category>javascript</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/479</guid>
      <comments>https://okimaru.tistory.com/entry/Javascript-%EC%A0%91%EA%B7%BC%EC%84%B1-%EA%B2%BD%EA%B3%A0focus-management#entry479comment</comments>
      <pubDate>Fri, 17 Apr 2026 13:32:53 +0900</pubDate>
    </item>
    <item>
      <title>async await 사용 시 promise 의 resolve 와 return; 차이</title>
      <link>https://okimaru.tistory.com/entry/async-await-%EC%82%AC%EC%9A%A9-%EC%8B%9C-promise-%EC%9D%98-resolve-%EC%99%80-return-%EC%B0%A8%EC%9D%B4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;new Promise 로 return 을 하는 함수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise 함수 내부에서 jquery 의 ajax 를 통해 api 요청을 한 후 count 값이 0일 경우 &quot;return;&quot; 을 사용했더니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;promise 를 await 로 호출한 부분 이후의 로직이 실행되지 않았다.&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;이유는 new Promise 가 resolve 또는 reject 를 줄 때 까지 await 는 계속 대기하고 있기 때문에&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;new Promise 함수 예시&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773117363558&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function checkData() {
    return new Promise((resolve, reject) =&amp;gt; {
        $.ajax({
            url: '/api/data',
            success: (response) =&amp;gt; {
                if (response.dataCnt === 0) {
                    return;  // ❌ Promise resolve 안됨, await 대기
                }
                resolve(response);  // ✅ 올바른 방법
            }
        });
    });
}

// 호출
async function main() {
    const result = await checkData();  // dataCnt=0이면 여기서 영원히 대기!
    console.log('이 코드는 실행 안됨');  // ❌
}&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해결 방법 3가지&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. resolve(undefined) 또는 return undefined;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773117564562&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;success: (response) =&amp;gt; {
    if (response.dataCnt === 0) {
        resolve();  // ✅ 빈 값으로 resolve
        // 또는 return undefined;  // ✅ Promise가 undefined로 resolve됨
    } else {
        resolve(response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 조건부 resolve(권장)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773117701978&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;success: (response) =&amp;gt; {
    if (response.dataCnt === 0) {
        resolve({ dataCnt: 0 });  // 명시적 상태 전달
        return;
    }
    resolve(response);
}&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;return; 이 아닌 resolve&amp;nbsp; 상태 전달을 통해 await 에게 알려준다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;return; 을 하게 되면 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;함수만 종료되고 await 는 계속 대기상태에 빠진다.&lt;/span&gt;&lt;/b&gt;&lt;/span&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. async/await 으로 리팩토링(가장 깔끔)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773117866545&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function checkData() {
    const response = await $.ajax({ url: '/api/data' });
    if (response.dataCnt === 0) {
        return { dataCnt: 0 };  // ✅ 자동으로 Promise resolve됨
    }
    return response;
}&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;3번째 방법은 new Promise 가 아닌 await + ajax 방식으로 바로 사용하는게 깔끔하다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;$.ajax() 는 Promise 를 반환하기 때문에 굳이 new Promise 를 사용할 필요가 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;return 하면 자동으로 Promise 가 resolve 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;new Promise 는 jQuery 이전 XMLHttpRequest 시대에 필요했던 패턴&lt;/span&gt;&lt;/b&gt;이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;호출 측 처리&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773120405515&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function main() {
    const result = await checkData();
    if (result.dataCnt === 0) {
        console.log('데이터 없음');
        return;  // 여기서도 안전
    }
    console.log('데이터 처리:', result);  // ✅ 정상 실행
}&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;여기서 await 로 요청했을 때 정상적으로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JavaScript/javascript</category>
      <category>$.ajax</category>
      <category>ajax</category>
      <category>javascript</category>
      <category>jquery</category>
      <category>new Promise</category>
      <category>reject</category>
      <category>resolve</category>
      <category>spring boot2</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/478</guid>
      <comments>https://okimaru.tistory.com/entry/async-await-%EC%82%AC%EC%9A%A9-%EC%8B%9C-promise-%EC%9D%98-resolve-%EC%99%80-return-%EC%B0%A8%EC%9D%B4#entry478comment</comments>
      <pubDate>Tue, 10 Mar 2026 14:38:39 +0900</pubDate>
    </item>
    <item>
      <title>img 태그 src 속성 아이폰 브라우저 CORS 문제 해결</title>
      <link>https://okimaru.tistory.com/entry/img-%ED%83%9C%EA%B7%B8-src-%EC%86%8D%EC%84%B1-%EC%95%84%EC%9D%B4%ED%8F%B0-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-CORS-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot, Thymeleaf 환경에서 URL 자체를 넘겨 HTML 의 img src 속성에 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근한 URL 과 불러오는 이미지 URL 자체의 도메인이 달랐기 때문에 CORS 문제가 터졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 폰에서는 이미지 자체를 잘 불러왔으나 아이폰에서는 안나와서 찾아보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 의 브라우저는 CORS 정책이 엄격하다고 한다.&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;그래서 여러가지 방법을 찾아보다가 가장 안정적이고 쉬운 방법은 backgroud-image css 설정을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용해 화면에 보여주는걸로 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 시도했던 방법은 &amp;lt;object&amp;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;b&gt;Object 태그 사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773029735772&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 기존 object에 class 추가 --&amp;gt;
&amp;lt;object class=&quot;product-image thumbnail&quot; 
        data=&quot;https://www.naver.com/image.jpg&quot; 
        type=&quot;image/jpeg&quot; 
        width=&quot;300&quot; height=&quot;200&quot;&amp;gt;
  &amp;lt;img src=&quot;https://www.naver.com/image.jpg&quot; alt=&quot;대체 이미지&quot;&amp;gt;
&amp;lt;/object&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;CSS background-image 사용 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773029844541&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 이렇게 하신 거죠? --&amp;gt;
&amp;lt;span th:style=&quot;'background-image: url(' + ${product.iconPath} + ')'&quot;
      class=&quot;icon-span&quot;&amp;gt;
&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;lt;object&amp;gt; VS span[background-image] 차이점 정리&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 항목 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&amp;lt;object&amp;gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; span + background-image &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;브라우저 CORS 검사&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;엄격 (iOS에서 차단)&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;없음&lt;/b&gt; (CSS 백그라운드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;JS DOM 조작&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;내부 콘텐츠로 childNodes 에러&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;없음&lt;/b&gt; (단순 CSS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;JS 에러 발생&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;null.childNodes 에러 가능&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;0% 발생&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;성능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;별도 리소스 로드&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;CSS 단일 요청&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;iOS Safari 호환&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;SVG에서 문제 잦음&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;완벽&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;캐싱&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;별도 이미지 캐시&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;CSS 배경화면 캐시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;background-image 장점&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CORS 우회 : background-image 는 &amp;lt;img&amp;gt; 처럼 CORS 검사 안받는다.&lt;/li&gt;
&lt;li&gt;JS 안전 : DOM 조작 없어서 childNodes 에러 0%&lt;/li&gt;
&lt;li&gt;iOS 완벽 : Safari / Chrome 둘 다 문제없음&lt;/li&gt;
&lt;li&gt;간단 : Thymeleaf 한 줄로 해결&lt;/li&gt;
&lt;li&gt;유연 : hover, active 등 CSS 효과 무한&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;성능 최적화 팁&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773030071733&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;span th:style=&quot;'background-image: url(' + ${product.iconPath} + ');background-size:contain'&quot;
      class=&quot;icon-span&quot;
      th:title=&quot;${product.name}&quot;
      th:attr=&quot;data-icon=${product.iconPath}&quot;&amp;gt;
&amp;lt;/span&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;span + background-image 는 실무에서 크로스 브라우저 이미지 표시의 고전적인 최선책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 에러도 없고 iOS 도 완벽하고 유지보수도 쉽다.&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;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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>HTML</category>
      <category>background-image</category>
      <category>Spring</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/477</guid>
      <comments>https://okimaru.tistory.com/entry/img-%ED%83%9C%EA%B7%B8-src-%EC%86%8D%EC%84%B1-%EC%95%84%EC%9D%B4%ED%8F%B0-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-CORS-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0#entry477comment</comments>
      <pubDate>Mon, 9 Mar 2026 13:21:42 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security OAuth2.0</title>
      <link>https://okimaru.tistory.com/entry/OIDC-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%B4%EB%9E%80</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1️⃣ OAuth 2.0이란 무엇인가?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 권한 위임(Authorization) 프로토콜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,   사용자의 비밀번호를 공유하지 않고&amp;nbsp;   Access Token 을 이용해   다른 서버의 리소스(API) 에 접근하도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 표준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsM7Pd/dJMcacCe7k0/MORfGRzvcqOO49l3wVpzkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsM7Pd/dJMcacCe7k0/MORfGRzvcqOO49l3wVpzkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsM7Pd/dJMcacCe7k0/MORfGRzvcqOO49l3wVpzkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsM7Pd%2FdJMcacCe7k0%2FMORfGRzvcqOO49l3wVpzkK%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;576&quot; height=&quot;432&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8xyqd/dJMcacbatTs/0KwkIvypsUnBLO2IxxYp00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8xyqd/dJMcacbatTs/0KwkIvypsUnBLO2IxxYp00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8xyqd/dJMcacbatTs/0KwkIvypsUnBLO2IxxYp00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8xyqd%2FdJMcacbatTs%2F0KwkIvypsUnBLO2IxxYp00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;429&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;457&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;size18&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;OAuth 2.0이 해결하는 문제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 사이트에서 이런 버튼을 본적 있을것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;Google 로그인&lt;/li&gt;
&lt;li&gt;Kakao 로그인&lt;/li&gt;
&lt;li&gt;GitHub 로그인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 사용자는 아이디/비밀번호를 그 서비스에 직접 입력하지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 인증 서버가 대신 인증을 해준다.&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;&lt;span style=&quot;color: #dddddd;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #666666;&quot;&gt;사용자&amp;nbsp;&amp;rarr;&amp;nbsp;내&amp;nbsp;서비스&amp;nbsp;&amp;rarr;&amp;nbsp;Google&amp;nbsp;로그인&amp;nbsp;&amp;rarr;&amp;nbsp;Google&amp;nbsp;인증&amp;nbsp;&amp;rarr;&amp;nbsp;내&amp;nbsp;서비스&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 서비스는 사용자의 비밀번호를 몰라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google 이 인증한 사용자라는 것만 확인하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  이것이 &lt;b&gt;OAuth 2.0의 핵심 개념 = 권한 위임(Authorization Delegation)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 2️⃣ OAuth 2.0 주요 구성요소 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.093%;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.7907%;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;Resource Owner&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;Client&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;사용자의 리소스에 접근하려는 어플리케이션(우리 Spring 서버)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;Authorization Server&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;로그인/동의를 받고 Access Token 을 발급하는 서버 (Keycloak, Auth Server)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.093%;&quot;&gt;Resource Server&lt;/td&gt;
&lt;td style=&quot;width: 72.7907%;&quot;&gt;API 서버&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 3️⃣ OAuth2 로그인 흐름 (Spring Security 기준) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 &quot;Google 로그인&quot; 클릭&lt;/li&gt;
&lt;li&gt;Spring Security &amp;rarr; Google 로그인 페이지 redirect(Google Authorization Server 로 redirect)&lt;/li&gt;
&lt;li&gt;사용자가 Google 로그인&lt;/li&gt;
&lt;li&gt;Google &amp;rarr; authorization code 반환&lt;br /&gt;=&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Google 에서 로그인 후 Authorication Code 를 우리 시스템의 callback URL 로 반환&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; (/login/oauth2/code/google) 로 반환&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Spring Security 서버 &amp;rarr; access token 요청 (autho)&lt;br /&gt;=&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Spring Security 의 OAuth2LoginAuthenticationFilter 가 Authorication Code 를 Google 에 전송해&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; Access Token 획득&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;access token 으로 사용자 정보 요청&lt;br /&gt;=&amp;gt; &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Access Token 으로 Google 사용자 정보(이메일, 프로필 등) 로드 후 세션 생성&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Spring Security &amp;rarr; 로그인 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1773208862239&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;사용자 &amp;rarr; Spring Boot (/oauth2/authorization/google)
     &amp;darr;
Spring Security &amp;rarr; Google Auth Server (리다이렉트)
     &amp;darr;
Google &amp;rarr; Spring Boot (/login/oauth2/code/google?code=ABC123)  
     &amp;darr;  
Spring Security &amp;rarr; Google (code=ABC123 &amp;rarr; access_token 교환) ✓
     &amp;darr;
Spring Security &amp;rarr; 사용자 정보 로드 &amp;rarr; 로그인 성공!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요약&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authorization Code &amp;rarr; Access Token &amp;rarr; User Info&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 4️⃣ Spring Security에서 OAuth2 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 에서는 spring-security-oauth2-client 로 쉽게 구현한다.&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;Spring Security 는 spring-boot-starter-oauth2-client 와 spring-security-oauth2 의존성을 추가하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2 로그인을 쉽게 설정할 수 있다. 사용자가 소셜 로그인 버튼을 클릭하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/oauth2/authorization/{provider} 로 요청이 가고, OAuth2LoginAuthenticationFilter 가 Authorization Code 를 받아&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token 을 교환한 후 사용자 정보를 로드한다.&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 구성 요소&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ClientRegistration&lt;/b&gt;: application.yml 에서 client-id, client-secret, redirect-uri 등을 등록한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OAuth2UserService&lt;/b&gt;: loadUser() 에서 외부 사용자 정보를 커스텀 처리(예 : DB 저장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SecurityConfig&lt;/b&gt;: .oauth2Login() 으로 필터 체인에 OAuth2 를 추가하고, 성공/실패 핸들러를 정의한다.&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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;application.yml 설정 예시&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773207559476&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: your-client-id
            client-secret: your-secret
            redirect-uri: &quot;{baseUrl}/login/oauth2/code/{registrationId}&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 URL : /oauth2/authorication/google&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5️⃣ OAuth2 vs JWT (헷갈리는 부분)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; OAuth2 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; JWT &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;역할&lt;/td&gt;
&lt;td&gt;권한 위임 프로토콜&lt;/td&gt;
&lt;td&gt;토큰 포맷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;목적&lt;/td&gt;
&lt;td&gt;외부 인증&lt;/td&gt;
&lt;td&gt;인증 정보 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;예&lt;/td&gt;
&lt;td&gt;Google 로그인&lt;/td&gt;
&lt;td&gt;access token&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 6️⃣ Spring Security에서 많이 사용하는 방식 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773209553794&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OAuth2 Login
      &amp;darr;
사용자 정보 가져옴
      &amp;darr;
우리 DB에 사용자 저장
      &amp;darr;
JWT 발급
      &amp;darr;
JWT로 API 인증&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_1773209566670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OAuth2 = 로그인
JWT = API 인증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 7️⃣ OAuth2를 사용하는 대표 서비스 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Login&lt;/li&gt;
&lt;li&gt;Kakao Login&lt;/li&gt;
&lt;li&gt;Naver Login&lt;/li&gt;
&lt;li&gt;Apple Login&lt;/li&gt;
&lt;li&gt;GitHub Login&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;   Spring Security에서 OAuth2 핵심 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1773209636525&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;외부 인증 서버에게 로그인을 맡기고
우리 서버는 토큰만 받아 인증 처리!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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 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;</description>
      <category>Java/Spring</category>
      <category>oauth2</category>
      <category>Spring boot</category>
      <category>spring security</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/476</guid>
      <comments>https://okimaru.tistory.com/entry/OIDC-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%B4%EB%9E%80#entry476comment</comments>
      <pubDate>Tue, 3 Mar 2026 13:40:23 +0900</pubDate>
    </item>
    <item>
      <title>HandlerMethodArgumentResolver 란?</title>
      <link>https://okimaru.tistory.com/entry/HandlerMethodArgumentResolver-%EB%9E%80</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HandlerMethodArgumentResolver 란?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 메서드의 파라미터를 직접 해석해서 객체로 만들어주는 확장 포인트 인터페이스다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해, Spring MVC 에서 컨트롤러 메서드의 매개변수를 HTTP 요청에서 실제 인자 값으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환하는 인터페이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet 이 HandlerMapping 에서 적절한 Mapping URL 을 찾아내고 HanlderAdapter 를 통해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 컨트롤러 메서드를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 컨트롤러 메서드를 호출하기 전 @RequestParam, @PathVariable, @RequestBody 등의 데이터들을&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 1️⃣ 왜 필요한가? (사용 이유) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1767770948912&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/users&quot;)
public String users(
    @RequestParam String id,
    @RequestHeader(&quot;Authorization&quot;) String token,
    HttpServletRequest request
)&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; ❌ 문제점 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러마다 같은 파라미터 파싱 코드 반복&lt;/li&gt;
&lt;li&gt;인증 정보, 사용자 정보, 헤더 값 처리 로직이 컨트롤러에 섞임&lt;/li&gt;
&lt;li&gt;테스트 어려움 / 관심사 분리 안됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   그래서 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 파라미터 하나로 의미 있는 객체를 자동 주입하고 싶을 때 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;HandlerMethodArgumentResolver를 사용&lt;/span&gt;&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 2️⃣ HandlerMethodArgumentResolver 인터페이스 &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1767771054384&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerMethodArgumentResolver {

    boolean supportsParameter(MethodParameter parameter);

    Object resolveArgument(
        MethodParameter parameter,
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest,
        WebDataBinderFactory binderFactory
    ) throws Exception;
}&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 핵심 메서드 2개&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 메서드 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 역할 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;supportsParameter()&lt;/td&gt;
&lt;td&gt;이 파라미터를 내가 처리할 수 있는지 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resolveArgument()&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 3️⃣ 동작 흐름 (중요 ⭐)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1767771162935&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP 요청
 &amp;darr;
DispatcherServlet
 &amp;darr;
HandlerMapping
 &amp;darr;
HandlerAdapter (Interceptor 처리 후 ArgumentResolver 체인 실행)
 &amp;darr;
컨트롤러 메서드 호출 전
 &amp;darr;
HandlerMethodArgumentResolver 목록 순회
   └ supportsParameter() == true ?
        └ resolveArgument() 실행
 &amp;darr;
컨트롤러 메서드 실행&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 4️⃣ 실제 예제 (가장 이해 잘 되는 예) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;   목표 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767771217328&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/me&quot;)
public String me(@LoginUser User user) {
    return user.getName();
}&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 4-1️⃣ 커스텀 어노테이션 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767771254232&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 4-2️⃣ Resolver 구현 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767771292971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CustomResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		System.out.println(&quot;parameter === &quot;+parameter.getParameterName());
		System.out.println(&quot;parameter === &quot;+parameter.getParameterType());
		System.out.println(&quot;parameter === &quot;+parameter.getParameterType().equals(int.class));
		return parameter.hasMethodAnnotation(LoginUser.class);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		HttpServletRequest request =
            (HttpServletRequest) webRequest.getNativeRequest();

        // 예: 세션에서 사용자 조회
        HttpSession session = request.getSession(false);
        return session != null ? session.getAttribute(&quot;LOGIN_USER&quot;) : null;
	}
}&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 4-3️⃣ Spring MVC에 등록 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767771336776&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
	
	@Autowired
	CustomResolver customResolver;
	
	@Override
	public void addArgumentResolvers(List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {
		resolvers.add(customResolver);
	}
}&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 4-4️⃣ 컨트롤러 사용 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767771490316&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/me&quot;)
public String me(@LoginUser User user) {
    return user.getName();
}&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 5️⃣ 언제 쓰면 좋은가? &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; ✅ 쓰면 좋은 경우 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 사용자 주입(@LoginUser)&lt;/li&gt;
&lt;li&gt;JWT &amp;rarr; User 객체 변환&lt;/li&gt;
&lt;li&gt;공통 헤더 파싱( @ClientIp, @ApiVersion )&lt;/li&gt;
&lt;li&gt;Request &amp;rarr; 도메인 객체 변환 로직이 반복될 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; ❌ 굳이 안 써도 되는 경우 &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순 @RequestParam, @PathVariable&lt;/li&gt;
&lt;li&gt;컨트롤러마다 다른 파싱 로직&lt;/li&gt;
&lt;li&gt;일회성 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 6️⃣ Spring Security 와의 관계 (중요) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1767771825323&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String test(@AuthenticationPrincipal UserDetails user)&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;➡ 내부적으로 HandlerMethodArgumentResolver 구현체가 있음&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;</description>
      <category>Java/Spring</category>
      <category>HandlerMethodArgumentResolver</category>
      <category>Spring</category>
      <category>Spring boot</category>
      <author>Z_Z</author>
      <guid isPermaLink="true">https://okimaru.tistory.com/475</guid>
      <comments>https://okimaru.tistory.com/entry/HandlerMethodArgumentResolver-%EB%9E%80#entry475comment</comments>
      <pubDate>Wed, 7 Jan 2026 16:44:34 +0900</pubDate>
    </item>
  </channel>
</rss>