<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yosi</title>
    <link>https://yosi42.tistory.com/</link>
    <description>yosi 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 29 Jul 2026 09:02:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yosi</managingEditor>
    <image>
      <title>yosi</title>
      <url>https://tistory1.daumcdn.net/tistory/8881801/attach/9e574e4be5c341448eda37145b9b68a7</url>
      <link>https://yosi42.tistory.com</link>
    </image>
    <item>
      <title>TCP 3-Way Handshake</title>
      <link>https://yosi42.tistory.com/12</link>
      <description>&lt;div style=&quot;display: none;&quot; aria-hidden=&quot;true&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L4LXH/dJMcaccSlEq/ATootRihrQTwnVnqxwszo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L4LXH/dJMcaccSlEq/ATootRihrQTwnVnqxwszo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L4LXH/dJMcaccSlEq/ATootRihrQTwnVnqxwszo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL4LXH%2FdJMcaccSlEq%2FATootRihrQTwnVnqxwszo0%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 DNS 조회를 마치면 서버의 IP 주소를 알게 됩니다. 하지만 TCP를 사용하는 HTTP/1.1이나 HTTP/2 통신이라면 바로 HTTP 요청부터 보내지 않습니다. 먼저 클라이언트와 서버 사이에 TCP 연결을 만듭니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회선 교환, 가상 회선, TCP 연결은 어떻게 다를까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;회선 교환(Circuit Switching)&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;가상 회선(Virtual Circuit)&lt;/b&gt;은 데이터를 패킷으로 보내면서도 먼저 논리적 경로를 설정합니다. 중간 스위치나 라우터는 회선 식별자와 다음 홉 같은 상태를 유지하고, 패킷은 설정된 논리적 경로를 따라갑니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;통신 전에 준비하는 것&lt;/th&gt;
&lt;th&gt;중간 네트워크의 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;회선 교환&lt;/td&gt;
&lt;td&gt;대역폭&amp;middot;시간 슬롯 같은 실제 자원&lt;/td&gt;
&lt;td&gt;통신이 끝날 때까지 전용 자원을 유지합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가상 회선 패킷 교환&lt;/td&gt;
&lt;td&gt;논리적 경로와 회선 식별자&lt;/td&gt;
&lt;td&gt;중간 장비가 가상 회선별 전달 상태를 유지합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCP 연결&lt;/td&gt;
&lt;td&gt;순서 번호, 송수신 버퍼, 윈도우 같은 종단 상태&lt;/td&gt;
&lt;td&gt;라우터는 TCP 연결 상태를 모르며 IP 데이터그램을 전달합니다.&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;TCP는 애플리케이션에 두 종단 사이의 가상 회선처럼 보이는 서비스를 제공합니다. 하지만 네트워크 계층의 가상 회선처럼 라우터에 고정 경로를 설정하거나 대역폭을 예약하지는 않습니다. 정확히는 클라이언트와 서버의 TCP가 연결 상태를 유지하는 &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;이 상태를 바탕으로 TCP는 바이트의 순서를 맞추고 ACK가 오지 않은 데이터를 재전송합니다. 다만 어떤 장애에서도 무조건 전달한다는 뜻은 아닙니다. 재전송으로 복구하지 못하면 연결은 Timeout이나 오류로 끝날 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SYN, SYN-ACK, ACK는 무엇을 확인할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 초기 순서 번호를 &lt;code&gt;x&lt;/code&gt;, 서버의 초기 순서 번호를 &lt;code&gt;y&lt;/code&gt;라고 해보겠습니다.&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;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;보내는 값&lt;/th&gt;
&lt;th&gt;확인하는 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SYN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클라이언트 &amp;rarr; 서버&lt;br /&gt;&lt;code&gt;SEQ=x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;연결을 요청하고 클라이언트의 초기 순서 번호를 알립니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SYN-ACK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서버 &amp;rarr; 클라이언트&lt;br /&gt;&lt;code&gt;SEQ=y, ACK=x+1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클라이언트의 SYN을 확인하고 서버의 초기 순서 번호를 알립니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ACK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클라이언트 &amp;rarr; 서버&lt;br /&gt;&lt;code&gt;ACK=y+1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;클라이언트가 서버의 SYN과 초기 순서 번호를 받았음을 확인합니다.&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;SYN은 실제 데이터가 없어도 순서 번호 공간에서 한 칸을 사용합니다. 그래서 상대의 SYN을 확인하는 ACK에는 초기 순서 번호에 &lt;code&gt;1&lt;/code&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;세 번째 ACK에는 애플리케이션 데이터를 함께 실을 수 있습니다. 서버의 초기 순서 번호를 확인하는 ACK과 첫 데이터를 같은 세그먼트에 담을 수 있다는 뜻입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 두 번이 아니라 세 번일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SYN &amp;rarr; SYN-ACK&lt;/code&gt; 두 단계에서 끝내면 클라이언트는 서버가 자신의 SYN을 받았다는 사실을 알 수 있습니다. 하지만 서버는 클라이언트가 서버의 SYN과 초기 순서 번호를 받았는지 확인할 수 없습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 ACK가 서버에 도착해야 양쪽 모두 &amp;ldquo;상대가 내 초기 순서 번호를 받았고, 나도 상대의 초기 순서 번호를 받았다&amp;rdquo;는 사실을 확인합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 네트워크에 늦게 남아 있던 과거의 중복 SYN을 새 연결 요청으로 잘못 받아들이는 문제도 줄입니다. 서버가 SYN만 받고 연결이 완성됐다고 판단하지 않는 이유입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 설정 실패는 FIN 종료와 별개입니다. SYN이나 SYN-ACK을 재전송하다가 Timeout으로 포기할 수 있으며, 반드시 FIN 4-Way Handshake로 정리해야 하는 것은 아닙니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새 연결을 계속 만들면 어떤 비용이 들까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-Way Handshake는 클라이언트 기준 최소 &lt;b&gt;1 RTT&lt;/b&gt;가 필요합니다. 요청할 때마다 새 TCP 연결을 만들면 애플리케이션 데이터를 보내기 전에 이 왕복 비용이 반복됩니다.&lt;/p&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 연결은 TCP Handshake 뒤에 DB 프로토콜의 인증 과정까지 필요할 수 있습니다. 그래서 백엔드에서는 쿼리마다 연결을 새로 만들지 않고 Connection Pool에 연결을 유지한 뒤 필요할 때 빌려 쓰고 반환합니다. 이미 만들어진 연결을 재사용해 Handshake 비용을 줄이는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 3-Way Handshake는 단순한 인사가 아닙니다. 클라이언트와 서버가 서로의 초기 순서 번호를 교환하고, 상대가 그 번호를 받았다는 사실까지 확인해 신뢰적인 전송의 기준점을 맞추는 과정입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SYN&lt;/code&gt;: 클라이언트의 초기 순서 번호 전달&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SYN-ACK&lt;/code&gt;: 클라이언트 번호 확인과 서버 번호 전달&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ACK&lt;/code&gt;: 서버 번호를 받았다는 마지막 확인&lt;/li&gt;
&lt;li&gt;실무 비용: 새 연결마다 최소 1 RTT, Connection Pool로 재사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9293.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 9293 &amp;mdash; Transmission Control Protocol&lt;/a&gt;&lt;/p&gt;</description>
      <category>CS/네트워크</category>
      <category>3wayhandshake</category>
      <category>ConnectionPool</category>
      <category>isn</category>
      <category>rtt</category>
      <category>tcp</category>
      <category>네트워크</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/12</guid>
      <comments>https://yosi42.tistory.com/12#entry12comment</comments>
      <pubDate>Fri, 24 Jul 2026 20:27:50 +0900</pubDate>
    </item>
    <item>
      <title>3 - 스프링 컨텍스트 빈 작성</title>
      <link>https://yosi42.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;앞 장에서는 객체를 스프링 컨텍스트에 등록하는 방법을 살펴봤습니다. 이제 컨텍스트 안에 &lt;code&gt;Person&lt;/code&gt; 빈과 &lt;code&gt;Parrot&lt;/code&gt; 빈이 있다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 객체가 각각 존재하는 것만으로는 &lt;code&gt;Person&lt;/code&gt;이 &lt;code&gt;Parrot&lt;/code&gt;을 사용할 수 없습니다. &lt;code&gt;Person&lt;/code&gt;이 필요로 하는 &lt;code&gt;Parrot&lt;/code&gt; 참조를 연결해야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;빈을 등록하는 것이 객체를 준비하는 단계라면, 의존성 주입은 준비된 객체들이 함께 일하도록 연결하는 단계입니다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epyKPn/dJMcafOgRQR/cx7ShWuT4ub2TYcdL5rRb1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epyKPn/dJMcafOgRQR/cx7ShWuT4ub2TYcdL5rRb1/img.jpg&quot; data-alt=&quot;스프링 컨테이너는 빈을 생성할 뿐 아니라 빈 사이의 의존성도 연결한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epyKPn/dJMcafOgRQR/cx7ShWuT4ub2TYcdL5rRb1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepyKPn%2FdJMcafOgRQR%2Fcx7ShWuT4ub2TYcdL5rRb1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;스프링 IoC 컨테이너가 Person 빈과 Parrot 빈의 의존성을 연결하는 구조&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;800&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&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;의존성 주입, DI는 객체가 의존성을 직접 생성하거나 컨테이너에서 찾는 대신 필요한 대상을 선언하고, 스프링 컨테이너가 그 값을 제공하는 방식입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. @Bean 메서드에서 다른 빈 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 설정 클래스에서는 두 가지 방식으로 &lt;code&gt;@Bean&lt;/code&gt; 사이의 관계를 표현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 @Bean 메서드를 직접 호출하기&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Configuration
public class ProjectConfig {

    @Bean
    public Parrot parrot() {
        return new Parrot(&quot;Koko&quot;);
    }

    @Bean
    public Person person() {
        return new Person(&quot;Ella&quot;, parrot());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;person()&lt;/code&gt;이 &lt;code&gt;parrot()&lt;/code&gt;을 호출하므로 일반 자바 코드만 보면 &lt;code&gt;Parrot&lt;/code&gt;이 호출할 때마다 새로 생성될 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정의 &lt;code&gt;@Configuration&lt;/code&gt; 클래스는 런타임 프록시 처리를 받습니다. 같은 설정 클래스 안에서 다른 &lt;code&gt;@Bean&lt;/code&gt; 메서드를 호출하면 스프링이 호출을 가로채 컨테이너가 관리하는 빈을 반환합니다. 기본 singleton 범위라면 &lt;code&gt;Person&lt;/code&gt;에 연결되는 객체와 컨텍스트에서 조회한 &lt;code&gt;Parrot&lt;/code&gt;은 같은 인스턴스입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; &lt;code&gt;@Configuration(proxyBeanMethods = false)&lt;/code&gt; 또는 &lt;code&gt;@Component&lt;/code&gt; 안의 &lt;code&gt;@Bean&lt;/code&gt;처럼 lite mode로 처리되면 직접 호출은 평범한 자바 메서드 호출입니다. 이때는 호출할 때마다 새 인스턴스가 생성될 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Bean 메서드 매개변수로 받기&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Configuration
public class ProjectConfig {

    @Bean
    public Parrot parrot() {
        return new Parrot(&quot;Koko&quot;);
    }

    @Bean
    public Person person(Parrot parrot) {
        return new Person(&quot;Ella&quot;, parrot);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;person()&lt;/code&gt;은 자신이 &lt;code&gt;Parrot&lt;/code&gt;을 필요로 한다는 사실만 매개변수로 선언합니다. 스프링은 컨텍스트에서 주입 후보를 찾아 해당 매개변수에 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성이 메서드 시그니처에 드러나고 lite mode의 직접 호출 문제도 피할 수 있어 의도가 더 명확합니다. 의존 대상이 다른 &lt;code&gt;@Bean&lt;/code&gt;에서 만들어졌는지, 컴포넌트 스캔으로 등록됐는지도 &lt;code&gt;Person&lt;/code&gt; 생성 로직이 알 필요가 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. @Autowired로 컴포넌트의 의존성 주입하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 생성자, 필드, 메서드에 &lt;code&gt;@Autowired&lt;/code&gt;를 적용해 타입에 맞는 빈을 찾고 주입할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWYGh5/dJMb991xy3j/skA1nO81GtTfCNCj9maFtk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWYGh5/dJMb991xy3j/skA1nO81GtTfCNCj9maFtk/img.jpg&quot; data-alt=&quot;필수 의존성은 생성자 주입으로 드러내는 편이 명확하다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWYGh5/dJMb991xy3j/skA1nO81GtTfCNCj9maFtk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWYGh5%2FdJMb991xy3j%2FskA1nO81GtTfCNCj9maFtk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;필드 주입 생성자 주입 setter 주입 비교&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;800&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;필수 의존성은 생성자 주입으로 드러내는 편이 명확하다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필드 주입&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Component
public class Person {

    @Autowired
    private Parrot parrot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 짧지만 객체가 어떤 의존성을 반드시 필요로 하는지 생성자에서 알기 어렵습니다. 필드를 &lt;code&gt;final&lt;/code&gt;로 만들 수 없고, 스프링 컨테이너 없이 단위 테스트를 작성할 때 의존성을 넣기도 불편합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성자 주입&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Component
public class Person {

    private final Parrot parrot;

    public Person(Parrot parrot) {
        this.parrot = parrot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 주입은 의존성을 객체 생성 조건으로 만듭니다. 필드를 &lt;code&gt;final&lt;/code&gt;로 선언할 수 있고, 테스트에서도 &lt;code&gt;new Person(fakeParrot)&lt;/code&gt;처럼 스프링 없이 의존성을 명시적으로 전달할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대상 클래스에 생성자가 하나뿐이면 현재 스프링에서는 생성자의 &lt;code&gt;@Autowired&lt;/code&gt;를 생략해도 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;setter 주입&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Component
public class Person {

    private Parrot parrot;

    @Autowired
    public void setParrot(Parrot parrot) {
        this.parrot = parrot;
    }
}&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;setter 주입은 객체를 만든 뒤 의존성을 설정합니다. 선택적으로 바꿀 수 있어야 하는 프로퍼티에는 사용할 수 있지만, 필수 의존성이라면 불완전한 객체가 잠시 존재할 수 있고 &lt;code&gt;final&lt;/code&gt;로 고정할 수도 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 순환 의존성은 왜 위험한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Person&lt;/code&gt;을 만들려면 &lt;code&gt;Parrot&lt;/code&gt;이 필요하고, &lt;code&gt;Parrot&lt;/code&gt;을 만들려면 다시 &lt;code&gt;Person&lt;/code&gt;이 필요하다고 해 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.7;&quot;&gt;&lt;code&gt;Person ──필요──&amp;gt; Parrot
   ^                │
   └──────필요──────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 빈이 생성자로 서로를 요구하면 어느 쪽도 먼저 완성할 수 없습니다. 스프링은 이 생성자 순환 참조를 감지하고 &lt;code&gt;BeanCurrentlyInCreationException&lt;/code&gt;으로 실패합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setter 주입을 이용하면 일부 순환 참조를 기술적으로 구성할 수 있지만, 한 객체가 완전히 초기화되기 전에 다른 객체에 노출될 수 있습니다. 순환 의존성은 두 클래스의 책임이 강하게 얽혔다는 설계 신호이므로 공통 책임 분리, 이벤트 발행, 협력 방향 재설계로 끊는 편이 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 같은 타입의 빈이 여러 개라면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 &lt;code&gt;Parrot&lt;/code&gt; 타입의 빈이 두 개면 타입만으로는 어떤 빈을 주입할지 결정할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Bean
public Parrot koko() {
    return new Parrot(&quot;Koko&quot;);
}

@Bean
public Parrot miki() {
    return new Parrot(&quot;Miki&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 후보는 @Primary&lt;/h3&gt;
&lt;pre class=&quot;less&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Bean
@Primary
public Parrot koko() {
    return new Parrot(&quot;Koko&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 값을 주입하는 지점에 같은 타입 후보가 여러 개 있을 때 정확히 하나의 &lt;code&gt;@Primary&lt;/code&gt; 빈이 있으면 그 빈이 우선됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특정 후보는 @Qualifier&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.6;&quot;&gt;&lt;code&gt;@Component
public class Person {

    private final Parrot parrot;

    public Person(@Qualifier(&quot;miki&quot;) Parrot parrot) {
        this.parrot = parrot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Qualifier&lt;/code&gt;는 타입으로 찾은 후보를 다시 좁혀 주입 의도를 명시합니다. 매개변수 이름과 빈 이름이 우연히 일치하는 규칙에 기대기보다 선택 기준을 코드에 드러낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후보가 하나로 좁혀지지 않으면 스프링은 임의로 선택하지 않고 예외를 발생시킵니다. 잘못된 객체가 조용히 주입되는 것보다 안전한 실패입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨텍스트는 빈을 보관하는 공간에 그치지 않습니다. 각 빈이 필요로 하는 협력 대상을 찾아 객체 그래프를 조립합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Bean&lt;/code&gt; 메서드 매개변수는 설정 클래스에서 의존성을 명확하게 표현합니다.&lt;/li&gt;
&lt;li&gt;컴포넌트의 필수 의존성은 생성자 주입으로 드러내는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;단일 생성자라면 &lt;code&gt;@Autowired&lt;/code&gt;를 생략할 수 있습니다.&lt;/li&gt;
&lt;li&gt;순환 의존성은 우회하기보다 객체의 책임과 협력 방향을 다시 살펴봐야 합니다.&lt;/li&gt;
&lt;li&gt;같은 타입 후보가 여러 개면 &lt;code&gt;@Primary&lt;/code&gt; 또는 &lt;code&gt;@Qualifier&lt;/code&gt;로 선택 의도를 표현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 빈으로 등록하는 것이 스프링 구성의 시작이라면, 빈 사이의 관계를 분명하게 작성하는 것은 실제 애플리케이션을 조립하는 단계입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고한 문서&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Framework &amp;mdash; Dependency Injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/java/basic-concepts.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Framework &amp;mdash; @Bean and @Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Framework &amp;mdash; Using @Autowired&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired-qualifiers.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Framework &amp;mdash; Autowiring with Qualifiers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/스프링 교과서</category>
      <category>autowired</category>
      <category>Di</category>
      <category>spring</category>
      <category>빈</category>
      <category>스프링</category>
      <category>의존성주입</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/11</guid>
      <comments>https://yosi42.tistory.com/11#entry11comment</comments>
      <pubDate>Thu, 23 Jul 2026 01:19:25 +0900</pubDate>
    </item>
    <item>
      <title>주소창에 도메인을 입력하면, IP는 어디서 올까? DNS 조회의 전체 흐름</title>
      <link>https://yosi42.tistory.com/10</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CNkff/dJMcaglZxAe/iVYZHlhrnmjCHapUluFKu0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CNkff/dJMcaglZxAe/iVYZHlhrnmjCHapUluFKu0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CNkff/dJMcaglZxAe/iVYZHlhrnmjCHapUluFKu0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCNkff%2FdJMcaglZxAe%2FiVYZHlhrnmjCHapUluFKu0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;675&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 주소창에 &lt;code&gt;www.example.com&lt;/code&gt;을 입력하면 곧바로 웹 서버에 연결되는 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 네트워크는 도메인 이름만으로 목적지를 찾을 수 없습니다. 실제 통신을 시작하려면 먼저 IPv4 또는 IPv6 주소가 필요합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다면 브라우저는 누구에게 IP 주소를 물어보고, DNS 서버들은 어떻게 최종 답을 찾아낼까요?&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 브라우저 캐시부터 재귀 DNS 리졸버, Root DNS, TLD DNS, Authoritative DNS까지 이어지는 전체 조회 흐름을 따라갑니다. 마지막에는 TTL이 서버 이전에 미치는 영향과 DNS가 UDP뿐 아니라 TCP도 사용하는 이유까지 정리합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 브라우저는 도메인 이름만으로 접속할 수 없다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 사람이 읽기 쉬운 도메인 이름을 네트워크가 사용할 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;브라우저는 먼저 자체 캐시를 확인하고, 운영체제의 DNS 캐시와 이름 해석 기능을 사용합니다. 여기에도 답이 없으면 운영체제에 설정된 &lt;b&gt;재귀 DNS 리졸버&lt;/b&gt;에 최종 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;재귀 리졸버는 ISP, 회사 네트워크, 공유기 뒤의 DNS 서비스 또는 Google Public DNS 같은 공용 서비스가 제공할 수 있습니다. 웹 요청을 중계하는 프록시 서버와는 다른 구성 요소입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. DNS 서버의 실제 테이블은 어떻게 생겼을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 하나의 거대한 표에서 주소를 한 번에 찾지 않습니다. Root, TLD, Authoritative DNS가 자신이 책임지는 Resource Record를 나눠 가집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Root DNS가 가진 Resource Record 테이블&lt;/h3&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; min-width: 720px; font-size: 14px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background-color: #f4f6f8;&quot;&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 58px;&quot;&gt;Type&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Value&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 82px;&quot;&gt;TTL&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;NS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;ns1.com-tld.example.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;172800&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;.com&lt;/code&gt;은 이 TLD DNS가 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;ns1.com-tld.example.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;192.0.2.10&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;172800&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;TLD DNS로 가기 위한 Glue Record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리졸버가 &lt;code&gt;www.example.com. A&lt;/code&gt;를 물으면 Root DNS는 오른쪽 계층인 &lt;code&gt;com.&lt;/code&gt; 행을 찾습니다. 최종 웹 서버 IP 대신 &lt;code&gt;.com&lt;/code&gt; TLD DNS의 NS 레코드와 필요한 Glue Record를 반환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. &lt;code&gt;.com&lt;/code&gt; TLD DNS가 가진 Resource Record 테이블&lt;/h3&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; min-width: 720px; font-size: 14px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background-color: #f4f6f8;&quot;&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 58px;&quot;&gt;Type&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Value&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 82px;&quot;&gt;TTL&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;NS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;ns1.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;86400&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;example.com&lt;/code&gt; Zone의 책임 DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;ns1.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;192.0.2.53&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;86400&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;책임 DNS로 가기 위한 Glue Record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리졸버는 Root DNS가 알려 준 &lt;code&gt;192.0.2.10&lt;/code&gt;으로 이동해 같은 질문을 보냅니다. TLD DNS는 &lt;code&gt;example.com.&lt;/code&gt; 행을 찾아 이 Zone을 담당하는 Authoritative DNS를 안내합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. &lt;code&gt;example.com&lt;/code&gt; Authoritative DNS가 가진 Resource Record 테이블&lt;/h3&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; min-width: 720px; font-size: 14px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background-color: #f4f6f8;&quot;&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 58px;&quot;&gt;Type&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Value&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 82px;&quot;&gt;TTL&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;www.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;192.0.2.80&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;300&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;웹 서버의 IPv4 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;www.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;AAAA&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;2001:db8::80&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;300&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;웹 서버의 IPv6 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;app.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;CNAME&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;www.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;300&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;app&lt;/code&gt;을 &lt;code&gt;www&lt;/code&gt;의 별칭으로 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리졸버는 &lt;code&gt;192.0.2.53&lt;/code&gt;의 Authoritative DNS에 질의합니다. 이 서버는 이름 &lt;code&gt;www.example.com.&lt;/code&gt;과 요청 타입 &lt;code&gt;A&lt;/code&gt;가 모두 일치하는 행을 찾아 최종 IP &lt;code&gt;192.0.2.80&lt;/code&gt;을 반환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. 조회가 끝난 뒤 재귀 리졸버의 캐시 테이블&lt;/h3&gt;
&lt;div style=&quot;overflow-x: auto; margin: 18px 0;&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; min-width: 680px; font-size: 14px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background-color: #f4f6f8;&quot;&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Name&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 58px;&quot;&gt;Type&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;Value&lt;/th&gt;
&lt;th style=&quot;padding: 9px; white-space: nowrap; width: 96px;&quot;&gt;남은 TTL&lt;/th&gt;
&lt;th style=&quot;padding: 9px;&quot;&gt;출처&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;NS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;ns1.com-tld.example.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;최대 172800&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;Root DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;NS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;ns1.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;최대 86400&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;.com&lt;/code&gt; TLD DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;www.example.com.&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;&lt;code&gt;192.0.2.80&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 9px; white-space: nowrap;&quot;&gt;최대 300&lt;/td&gt;
&lt;td style=&quot;padding: 9px;&quot;&gt;Authoritative DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리졸버는 중간 위임 정보와 최종 A 레코드를 각각의 TTL 동안 캐시합니다. A 레코드만 만료되고 NS 캐시는 남아 있다면 Root부터 다시 시작하지 않고 Authoritative DNS에 곧바로 물을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 테이블의 어느 행을 따라가는가&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 18px; overflow-x: auto; line-height: 1.7;&quot;&gt;&lt;code&gt;질의: www.example.com. A

1) Root DNS 테이블
   com.  NS  ns1.com-tld.example.
   &amp;rarr; .com TLD DNS: 192.0.2.10

2) .com TLD DNS 테이블
   example.com.  NS  ns1.example.com.
   &amp;rarr; Authoritative DNS: 192.0.2.53

3) Authoritative DNS 테이블
   www.example.com.  A  192.0.2.80
   &amp;rarr; 최종 웹 서버 IP: 192.0.2.80

4) 재귀 리졸버 캐시
   중간 NS와 최종 A 레코드를 각 TTL 동안 저장
   &amp;rarr; 브라우저에 192.0.2.80 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; data-spacing-after-block=&quot;true&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Root와 TLD DNS는 질문과 일치하는 오른쪽 도메인 계층의 NS 행을 찾아 다음 DNS 서버를 안내합니다. Authoritative DNS에 도착한 뒤에야 전체 이름과 타입이 일치하는 A 행에서 최종 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;&lt;code&gt;www.example.com.&lt;/code&gt;의 마지막 점은 DNS 계층 최상단인 Root를 뜻합니다. 평소에는 생략하지만 완전한 도메인 이름에는 개념적으로 포함되어 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 재귀 질의와 반복 질의는 책임이 다르다&lt;/h2&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;재귀 리졸버가 Root, TLD, Authoritative DNS에 차례로 물으며 다음 질의 위치를 안내받는 것은 일반적으로 &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;Root DNS는 &lt;code&gt;.com&lt;/code&gt;을 담당하는 TLD DNS의 NS 레코드를 알려 줍니다.&lt;/li&gt;
&lt;li&gt;TLD DNS는 &lt;code&gt;example.com&lt;/code&gt;을 담당하는 Authoritative DNS의 NS 레코드를 알려 줍니다.&lt;/li&gt;
&lt;li&gt;Authoritative DNS는 자신이 책임지는 A&amp;middot;AAAA 또는 CNAME 레코드로 응답합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Root 같은 상위 서버가 모든 재귀 조회를 직접 처리하지 않기 때문에 부하와 장애 범위를 전 세계 DNS 계층에 분산할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. A, AAAA, NS, CNAME은 무엇이 다를까&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; font-size: 15px; margin: 20px 0;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;background-color: #f4f6f8;&quot;&gt;
&lt;th style=&quot;padding: 10px;&quot;&gt;레코드&lt;/th&gt;
&lt;th style=&quot;padding: 10px;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;호스트 이름을 IPv4 주소에 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;code&gt;AAAA&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;호스트 이름을 IPv6 주소에 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;code&gt;NS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;특정 도메인 또는 Zone을 담당하는 이름 서버를 가리킵니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;&lt;code&gt;CNAME&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding: 10px;&quot;&gt;한 호스트 이름을 다른 정식 호스트 이름의 별칭으로 연결합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authoritative DNS가 다음처럼 CNAME을 반환할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background: #f7f8fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; overflow-x: auto;&quot;&gt;&lt;code&gt;www.example.com CNAME app.hosting.net&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; data-spacing-after-block=&quot;true&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 재귀 리졸버는 &lt;code&gt;app.hosting.net&lt;/code&gt;의 A&amp;middot;AAAA 레코드가 캐시에 있는지 확인합니다. 없다면 &lt;code&gt;hosting.net&lt;/code&gt;에 대한 DNS 조회를 이어갑니다. 클라이언트가 실제 서버에 접속하려면 결국 IP 주소가 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. TTL이 끝나야 DNS 변경이 보이는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 응답의 Resource Record에는 TTL, Time To Live가 들어 있습니다. 재귀 리졸버는 레코드를 TTL 동안 캐시해 반복 조회의 지연과 DNS 서버의 부하를 줄입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;흔히 말하는 &amp;ldquo;DNS 전파 시간&amp;rdquo;도 중앙 서버가 변경을 전 세계에 밀어내는 시간이 아닙니다. 브라우저, 운영체제, 재귀 리졸버 등에 흩어진 기존 캐시가 각자의 TTL에 따라 만료되는 시간에 가깝습니다.&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;긴 TTL&lt;/b&gt;: 조회량과 지연은 줄지만 장애 조치와 주소 변경이 느립니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;짧은 TTL&lt;/b&gt;: 변경은 빠르게 반영되지만 DNS 질의량과 인프라 의존도가 커집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 서버 IP를 안전하게 이전하는 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 A 레코드의 TTL이 24시간인데 내일 서버 IP를 바꾼다고 가정해 보겠습니다. 변경 직전에 TTL을 5분으로 낮춰도 이미 캐시된 24시간은 소급해서 줄어들지 않습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이전 최소 24시간 전에 TTL을 5분처럼 짧게 낮춥니다.&lt;/li&gt;
&lt;li&gt;기존 24시간 캐시가 만료될 시간을 기다립니다.&lt;/li&gt;
&lt;li&gt;A&amp;middot;AAAA 레코드를 새 서버 IP로 변경합니다.&lt;/li&gt;
&lt;li&gt;짧은 TTL과 운영 여유 시간 동안 이전 서버와 새 서버를 함께 운영합니다.&lt;/li&gt;
&lt;li&gt;트래픽과 오류율을 확인한 뒤 이전 서버를 종료합니다.&lt;/li&gt;
&lt;li&gt;전환과 롤백 위험이 낮아지면 TTL을 다시 늘립니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 변경 후 사용자가 무조건 존재하지 않는 IP로 가는 것은 아닙니다. TTL이 남아 있는 동안에는 &lt;b&gt;이전 IP&lt;/b&gt;로 갑니다. 이전 서버를 너무 일찍 종료했을 때 연결 실패가 발생합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. DNS는 왜 UDP를 사용하고 언제 TCP로 바꿀까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 DNS 질의는 짧은 요청 하나와 응답 하나로 끝납니다. UDP를 사용하면 TCP 연결을 만들기 위한 Handshake와 연결 상태 관리 비용을 피할 수 있습니다. 응답이 유실되면 클라이언트가 다시 질의하거나 다른 DNS 서버를 시도할 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DNS가 UDP를 사용하는 이유는 IP 주소의 정확성이나 유실이 중요하지 않아서가 아닙니다.&lt;/b&gt; 짧은 요청&amp;middot;응답을 더 적은 비용으로 처리하고, 손실 시 DNS 계층에서 재시도할 수 있기 때문입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 UDP만 사용하는 프로토콜도 아닙니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UDP 응답이 너무 커서 잘리면 서버가 &lt;code&gt;TC&lt;/code&gt;, Truncated 플래그를 설정하고 클라이언트가 TCP로 다시 질의합니다.&lt;/li&gt;
&lt;li&gt;EDNS는 기존 UDP 응답 크기 한계를 확장해 TCP 전환을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;DNSSEC, AAAA, 대형 TXT 레코드 등으로 응답이 커지면 TCP가 필요할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Authoritative DNS 사이에서 Zone 전체를 옮기는 AXFR는 TCP를 사용합니다.&lt;/li&gt;
&lt;li&gt;현재 DNS 표준은 완전한 DNS 구현이 TCP도 지원하도록 요구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP도 연결 실패와 Timeout이 생길 수 있으므로 &amp;ldquo;오래 걸려도 무조건 받는 방식&amp;rdquo;은 아닙니다. 전송 방식의 보장과 애플리케이션이 결과를 끝내 얻는다는 보장은 구분해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 도메인 이름을 IP로 바꾸는 과정의 중심에는 재귀 DNS 리졸버가 있습니다. 리졸버는 캐시가 없으면 Root, TLD, Authoritative DNS를 반복적으로 조회하고, 얻은 A&amp;middot;AAAA 또는 CNAME 결과를 TTL 동안 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS의 계층 구조는 전 세계 이름 정보를 한 서버에 집중하지 않게 하고, 캐시는 조회 지연과 부하를 줄입니다. 대신 TTL 동안 이전 주소가 남을 수 있으므로 서버 이전에서는 DNS 변경뿐 아니라 기존 서버의 운영 기간까지 함께 설계해야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고한 표준&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1034.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 1034 &amp;mdash; Domain Names: Concepts and Facilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc1035.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 1035 &amp;mdash; Domain Names: Implementation and Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6891.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 6891 &amp;mdash; EDNS(0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc7766.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 7766 &amp;mdash; DNS over TCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5936.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 5936 &amp;mdash; DNS Zone Transfer Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5737.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 5737 &amp;mdash; IPv4 Address Blocks Reserved for Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc3849.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 3849 &amp;mdash; IPv6 Address Prefix Reserved for Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS/네트워크</category>
      <category>DNS</category>
      <category>tcp</category>
      <category>TTL</category>
      <category>udp</category>
      <category>네트워크</category>
      <category>재귀리졸버</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/10</guid>
      <comments>https://yosi42.tistory.com/10#entry10comment</comments>
      <pubDate>Thu, 23 Jul 2026 00:49:55 +0900</pubDate>
    </item>
    <item>
      <title>2 - 스프링 컨텍스트 빈 정의</title>
      <link>https://yosi42.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 Spring Bean으로 만든다는 것은 단순히 인스턴스를 컨테이너에 넣는 것이 아니라, &lt;b&gt;스프링이 그 객체를 어떻게 만들고 식별하고 관리할지 알 수 있도록 Bean Definition을 등록하는 일&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 자바 객체를 하나 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;PaymentClient paymentClient = new HttpPaymentClient();&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;하지만 이 객체를 생성했다고 해서 스프링이 자동으로 알게 되는 것은 아닙니다. 스프링이 의존성을 주입하고 생명주기를 관리하며 AOP 후처리를 적용하려면 먼저 이 객체에 대한 정의가 &lt;code&gt;ApplicationContext&lt;/code&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;처음에는 스프링 컨텍스트를 Bean을 담는 바구니로 이해해도 괜찮다. 다만 더 정확히 말하면 컨테이너는 완성된 객체만 보관하는 상자라기보다 &lt;b&gt;객체를 생성하고 관리하기 위한 설명서인 Bean Definition과, 그 설명서로 만들어진 Bean을 관리하는 곳&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 장에서 중요한 질문은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체를 어떤 방식으로 스프링의 관리 대상으로 등록할 것인가?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록 방식은 세 가지입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구성 클래스에서 &lt;code&gt;@Bean&lt;/code&gt; 메서드로 직접 등록합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Component&lt;/code&gt; 계열 애너테이션을 붙이고 컴포넌트 스캔으로 발견하게 합니다.&lt;/li&gt;
&lt;li&gt;조건과 데이터를 코드로 판단해 프로그래밍 방식으로 등록합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &lt;code&gt;@Bean&lt;/code&gt;: 객체 생성 과정을 내가 직접 설명한다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buMKbh/dJMcahegqh5/CFvV0jutoVzquozSqWFzIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buMKbh/dJMcahegqh5/CFvV0jutoVzquozSqWFzIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buMKbh/dJMcahegqh5/CFvV0jutoVzquozSqWFzIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuMKbh%2FdJMcahegqh5%2FCFvV0jutoVzquozSqWFzIK%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt;은 이 클래스가 Bean Definition의 출처라는 뜻이고, &lt;code&gt;@Bean&lt;/code&gt;은 해당 메서드가 반환하는 객체를 스프링이 관리하게 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
class PaymentConfig {

    @Bean
    PaymentClient paymentClient() {
        return new HttpPaymentClient();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 &lt;code&gt;paymentClient()&lt;/code&gt;를 평범한 비즈니스 메서드가 아니라 Bean을 만드는 팩터리 메서드로 해석합니다. 기본적으로 메서드 이름인 &lt;code&gt;paymentClient&lt;/code&gt;가 Bean 이름이 되고, 반환 타입과 생성 방법 등의 정보가 Bean Definition에 반영됩니다.&lt;/p&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;code&gt;@Component&lt;/code&gt;를 붙일 수 없는 객체도 등록할 수 있습니다.&lt;/li&gt;
&lt;li&gt;생성자에 복잡한 설정값을 전달하거나 팩터리 메서드를 호출할 수 있습니다.&lt;/li&gt;
&lt;li&gt;같은 타입의 Bean을 서로 다른 이름과 설정으로 여러 개 정의할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Bean마다 팩터리 메서드를 작성해야 하므로 등록할 객체가 많으면 구성 코드가 길어질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt;과 &lt;code&gt;@Bean&lt;/code&gt;은 무슨 관계일까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Bean&lt;/code&gt; 메서드는 일반 &lt;code&gt;@Component&lt;/code&gt; 클래스에도 선언할 수 있습니다. 하지만 보통은 &lt;code&gt;@Configuration&lt;/code&gt; 안에서 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt;의 기본 모드에서는 같은 구성 클래스 안의 &lt;code&gt;@Bean&lt;/code&gt; 메서드 호출을 컨테이너가 가로채 Scope와 생명주기 규칙을 지킨다. 반대로 &lt;code&gt;@Configuration(proxyBeanMethods = false)&lt;/code&gt; 또는 일반 컴포넌트의 &lt;code&gt;@Bean&lt;/code&gt;은 이른바 lite mode로 처리되어 메서드 직접 호출이 평범한 자바 호출처럼 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최근에는 Bean 사이의 의존성을 메서드 직접 호출보다 매개변수로 드러내는 방식이 이해하기 쉽습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
OrderService orderService(PaymentClient paymentClient) {
    return new OrderService(paymentClient);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 타입의 Bean이 여러 개라면&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eDfKGH/dJMcadCKyv7/kkQuPzBKelq7bfEQRCg0d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eDfKGH/dJMcadCKyv7/kkQuPzBKelq7bfEQRCg0d1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eDfKGH/dJMcadCKyv7/kkQuPzBKelq7bfEQRCg0d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeDfKGH%2FdJMcadCKyv7%2FkkQuPzBKelq7bfEQRCg0d1%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 인터페이스를 구현하는 Bean을 여러 개 등록하는 것은 자연스럽습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
class PaymentConfig {

    @Bean
    PaymentClient cardPaymentClient() {
        return new CardPaymentClient();
    }

    @Bean
    PaymentClient bankPaymentClient() {
        return new BankPaymentClient();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 등록이 아니라 &lt;b&gt;단일 &lt;code&gt;PaymentClient&lt;/code&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 후보 하나를 정한다: &lt;code&gt;@Primary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;주입 지점과 후보를 세밀하게 연결한다: &lt;code&gt;@Qualifier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;이름과 타입으로 직접 조회한다: &lt;code&gt;getBean(&quot;cardPaymentClient&quot;, PaymentClient.class)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;후보 전체가 필요하면 &lt;code&gt;List&amp;lt;PaymentClient&amp;gt;&lt;/code&gt; 또는 &lt;code&gt;Map&amp;lt;String, PaymentClient&amp;gt;&lt;/code&gt;로 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Primary&lt;/code&gt;는 같은 타입의 Bean 중 하나를 없애거나 나머지를 무효로 만드는 애너테이션이 아닙니다. &lt;b&gt;단일 의존성을 결정할 때 우선권을 주는 표시&lt;/b&gt;다. 컬렉션으로 주입하면 타입에 맞는 Bean은 모두 포함됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 컴포넌트 스캔: 클래스를 표시하고 스프링이 발견하게 한다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xivp4/dJMcahrOLaq/lDn4jK5ki8i4yeTw7nfK6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xivp4/dJMcahrOLaq/lDn4jK5ki8i4yeTw7nfK6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xivp4/dJMcahrOLaq/lDn4jK5ki8i4yeTw7nfK6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXivp4%2FdJMcahrOLaq%2FlDn4jK5ki8i4yeTw7nfK6K%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 직접 소유한 클래스라면 스테레오타입 애너테이션이 편리합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
class OrderService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@ComponentScan(basePackages = &quot;com.example.order&quot;)
class OrderConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 두 단계로 볼 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클래스에 &lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Controller&lt;/code&gt; 같은 스테레오타입을 붙여 후보임을 표시합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ComponentScan&lt;/code&gt;이 지정한 패키지를 탐색해 후보 클래스의 Bean Definition을 컨텍스트에 등록합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Controller&lt;/code&gt;도 결국 &lt;code&gt;@Component&lt;/code&gt;를 메타 애너테이션으로 가집니다. 기능상 모두 스캔 대상이지만 계층의 역할을 드러내므로 상황에 맞는 애너테이션을 사용하는 편이 좋습니다. 특히 &lt;code&gt;@Repository&lt;/code&gt;는 데이터 접근 예외 변환과도 연결됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스테레오타입은 같은 타입의 Bean을 하나만 만들 수 있을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표현은 조건을 붙여야 정확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 스캔은 기본적으로 &lt;b&gt;발견한 컴포넌트 클래스마다 하나의 Bean Definition&lt;/b&gt;을 등록합니다. 따라서 하나의 구체 클래스에 &lt;code&gt;@Component&lt;/code&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;하지만 스테레오타입 방식 자체가 같은 타입의 Bean 여러 개를 금지하는 것은 아닙니다. 같은 인터페이스를 구현한 여러 컴포넌트가 각각 발견될 수도 있고, Scope나 다른 등록 방식을 조합할 수도 있습니다. 핵심은 &amp;ldquo;스캔된 클래스 하나당 기본 Bean Definition 하나&amp;rdquo;라고 이해하는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@PostConstruct&lt;/code&gt;는 등록 방식이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@PostConstruct&lt;/code&gt;는 Bean을 컨텍스트에 등록하는 애너테이션이 아닙니다. Bean이 생성되고 의존성 주입이 끝난 뒤 초기화 작업을 실행하는 생명주기 콜백입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
class CacheWarmup {

    @PostConstruct
    void load() {
        // 초기 데이터 적재
    }
}&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;@Bean&lt;/code&gt;과 컴포넌트 스캔은 언제 선택할까&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기준&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@Bean&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;컴포넌트 스캔&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;생성 과정&lt;/td&gt;
&lt;td&gt;직접 제어한다&lt;/td&gt;
&lt;td&gt;기본적으로 컨테이너가 생성한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대상 클래스&lt;/td&gt;
&lt;td&gt;외부 라이브러리 포함&lt;/td&gt;
&lt;td&gt;주로 직접 수정 가능한 애플리케이션 클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;같은 구체 클래스의 여러 설정&lt;/td&gt;
&lt;td&gt;명시적으로 만들기 쉽다&lt;/td&gt;
&lt;td&gt;기본 스캔만으로는 한 정의가 생긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드량&lt;/td&gt;
&lt;td&gt;Bean마다 메서드가 필요하다&lt;/td&gt;
&lt;td&gt;후보 클래스에 애너테이션을 붙이면 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용하기 좋은 곳&lt;/td&gt;
&lt;td&gt;인프라 설정, 외부 SDK, 복잡한 생성&lt;/td&gt;
&lt;td&gt;Controller, Service, Repository 등 애플리케이션 구성 요소&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;code&gt;DataSource&lt;/code&gt;, HTTP Client, 외부 SDK처럼 생성 설정을 명시해야 하는 객체는 &lt;code&gt;@Bean&lt;/code&gt;으로 등록하는 조합이 흔합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프로그래밍 방식: 등록 조건 자체가 동적일 때 사용한다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blZ7kP/dJMcahyxaVZ/Sa8nk4EJAiamhXAUKVMIuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blZ7kP/dJMcahyxaVZ/Sa8nk4EJAiamhXAUKVMIuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blZ7kP/dJMcahyxaVZ/Sa8nk4EJAiamhXAUKVMIuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblZ7kP%2FdJMcahyxaVZ%2FSa8nk4EJAiamhXAUKVMIuk%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&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;등록할 Bean의 수나 종류가 런타임 입력, 설정 파일 또는 반복문 결과에 따라 달라진다면 프로그래밍 방식이 필요할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;var context = new AnnotationConfigApplicationContext();

for (ClientProperties client : clients) {
    context.registerBean(
        client.name(),
        PaymentClient.class,
        () -&amp;gt; new HttpPaymentClient(client.baseUrl())
    );
}

context.refresh();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;registerBean()&lt;/code&gt;의 주요 입력은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bean 이름&lt;/li&gt;
&lt;li&gt;Bean 클래스&lt;/li&gt;
&lt;li&gt;인스턴스를 만드는 &lt;code&gt;Supplier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Primary, Lazy 같은 Bean Definition 속성을 바꾸는 Customizer&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;context.registerBean(
    &quot;primaryPaymentClient&quot;,
    PaymentClient.class,
    () -&amp;gt; new HttpPaymentClient(baseUrl),
    definition -&amp;gt; definition.setPrimary(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;이 방식은 유연하지만 일반적인 서비스 객체까지 모두 코드로 등록하면 구성을 한눈에 파악하기 어려워집니다. &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;Spring Framework 7에는 프로그램 등록을 캡슐화하는 &lt;code&gt;BeanRegistrar&lt;/code&gt;와 &lt;code&gt;BeanRegistry&lt;/code&gt; API도 추가되었습니다. 다만 이 장의 핵심은 특정 API 이름보다 &amp;ldquo;선언만으로 표현하기 어려운 등록 규칙을 코드로 만들 수 있다&amp;rdquo;는 점입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 방법을 하나의 원리로 묶기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로는 애너테이션과 메서드가 서로 달라 보이지만 컨테이너 관점에서는 같은 목적을 가진다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean 메서드 ─────────────┐
                        │
컴포넌트 스캔  -────────────┼─&amp;rarr; Bean Definition 등록
                         │      &amp;darr;
registerBean() ──────────┘   Bean 생성&amp;middot;조립&amp;middot;관리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 객체를 누가 &lt;code&gt;new&lt;/code&gt; 했는지만 보는 것이 아닙니다. 스프링이 객체의 생성 방법과 식별자, Scope, 의존 관계 같은 메타데이터를 알고 관리 흐름에 참여하는지를 봐야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에게 객체를 관리해 달라고 하려면 그 객체의 존재와 생성 방법을 Bean Definition으로 알려 줘야 합니다. @Bean, 컴포넌트 스캔, 프로그래밍 등록은 그 정의를 전달하는 서로 다른 방법입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관점이 잡히면 &lt;code&gt;@Bean&lt;/code&gt;과 &lt;code&gt;@Component&lt;/code&gt;를 단순히 &amp;ldquo;둘 다 Bean을 만드는 애너테이션&amp;rdquo;으로 외우지 않게 됩니다. 어떤 객체를 등록하는지, 생성 과정을 얼마나 제어해야 하는지, 등록 규칙이 정적인지 동적인지를 보고 방식을 선택할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 질문&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;@Component&lt;/code&gt;와 &lt;code&gt;@Bean&lt;/code&gt;의 가장 큰 차이는 무엇인가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Component&lt;/code&gt;는 클래스 자체를 스캔 후보로 표시하고, &lt;code&gt;@Bean&lt;/code&gt;은 개발자가 작성한 팩터리 메서드의 반환 객체를 등록합니다. 수정할 수 없는 외부 클래스나 생성 로직을 직접 통제해야 하는 객체에는 &lt;code&gt;@Bean&lt;/code&gt;이 적합합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 타입의 Bean이 여러 개면 어떻게 되나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션 주입에는 모두 들어갈 수 있지만 단일 타입 주입은 후보를 하나로 결정해야 합니다. &lt;code&gt;@Primary&lt;/code&gt;, &lt;code&gt;@Qualifier&lt;/code&gt;, 명시적 이름 조회 등으로 의도를 알려야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모든 클래스를 Bean으로 등록하면 좋은가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아닙니다. 협력 객체나 인프라 의존성처럼 컨테이너의 조립과 생명주기 관리가 필요한 대상을 중심으로 등록합니다. 값 객체나 메서드 안에서 잠깐 쓰는 객체까지 Bean으로 만들면 컨테이너 의존성과 구성 복잡도만 커질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로그래밍 등록은 언제 필요한가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 데이터에 따라 여러 Bean을 반복해서 만들거나, 선언적 애너테이션만으로 표현하기 어려운 동적 등록 규칙이 있을 때 유용합니다. 일반적인 애플리케이션 Bean은 선언적 구성이 더 읽기 쉽습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 원리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일반 자바 객체와 Spring Bean의 차이는 컨테이너의 관리 대상인지에 있습니다.&lt;/li&gt;
&lt;li&gt;컨테이너는 Bean Definition을 바탕으로 Bean을 생성하고 조립하고 관리합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Bean&lt;/code&gt;은 생성 과정을 직접 제어하고, 컴포넌트 스캔은 후보 클래스를 자동 탐색합니다.&lt;/li&gt;
&lt;li&gt;같은 타입의 Bean이 여러 개일 수 있으며 단일 주입에는 선택 기준이 필요합니다.&lt;/li&gt;
&lt;li&gt;프로그래밍 등록은 동적인 등록 규칙이 있을 때 사용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/java/basic-concepts.html&quot;&gt;Basic Concepts: &lt;code&gt;@Bean&lt;/code&gt; and &lt;code&gt;@Configuration&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html&quot;&gt;Classpath Scanning and Managed Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/definition.html&quot;&gt;Bean Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired-primary.html&quot;&gt;Fine-tuning Autowiring with &lt;code&gt;@Primary&lt;/code&gt; or &lt;code&gt;@Fallback&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/support/GenericApplicationContext.html&quot;&gt;GenericApplicationContext API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/스프링 교과서</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/9</guid>
      <comments>https://yosi42.tistory.com/9#entry9comment</comments>
      <pubDate>Mon, 13 Jul 2026 22:51:52 +0900</pubDate>
    </item>
    <item>
      <title>1 - 스프링 생태계</title>
      <link>https://yosi42.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 중심은 웹&amp;middot;DB 기능의 모음이 아니라 애플리케이션 객체를 생성하고 연결하고 관리하는 IoC 컨테이너입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 스프링을 배울 때는 &lt;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt; 같은 애너테이션이나 REST API를 만드는 방법부터 보게 됩니다. 그러다 보면 스프링을 &amp;ldquo;백엔드 개발에 필요한 라이브러리를 모아 놓은 프레임워크&amp;rdquo; 정도로 이해하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MVC, 데이터 접근, 테스트 지원을 하나로 이어 주는 중심에는 스프링 코어와 IoC 컨테이너가 있습니다. 스프링을 이해한다는 것은 기능 목록을 외우는 것보다 &lt;b&gt;누가 객체를 만들고, 누가 의존성을 연결하며, 누가 객체의 수명주기를 관리하는가&lt;/b&gt;를 이해하는 데서 시작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 생태계를 하나의 구조로 보기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSmE8d/dJMb99NWX6t/WUbz5AHlAcdYUmexNTeBAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSmE8d/dJMb99NWX6t/WUbz5AHlAcdYUmexNTeBAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSmE8d/dJMb99NWX6t/WUbz5AHlAcdYUmexNTeBAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSmE8d%2FdJMb99NWX6t%2FWUbz5AHlAcdYUmexNTeBAk%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&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;h3 data-ke-size=&quot;size23&quot;&gt;Spring Core&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 기반입니다. 특히 &lt;code&gt;ApplicationContext&lt;/code&gt;로 대표되는 IoC 컨테이너가 객체를 생성하고 구성하고 조립합니다. 스프링이 관리하는 객체를 Bean이라고 부릅니다.&lt;/p&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;Spring Context: Bean과 의존 관계, 설정과 생명주기를 관리합니다.&lt;/li&gt;
&lt;li&gt;Spring AOP: 프록시 등을 이용해 로깅, 트랜잭션 같은 횡단 관심사를 메서드 실행 전후에 적용합니다.&lt;/li&gt;
&lt;li&gt;SpEL: 설정과 런타임 객체를 표현식으로 조회하거나 계산할 수 있는 표현 언어입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoC와 AOP를 같은 뜻으로 보면 안 됩니다. IoC는 객체 생성과 의존성 제어의 방향을 바꾸는 원리이고, AOP는 여러 기능에 반복되는 부가 로직을 분리하는 방법입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring MVC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet 기반 웹 애플리케이션을 만들기 위한 모듈입니다. HTTP 요청을 Controller에 연결하고, 요청 값을 객체로 변환하며, 검증 결과나 반환 값을 HTTP 응답으로 만드는 흐름을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC가 요청을 받아도 실제 비즈니스 로직을 담당하는 Service와 데이터 접근 객체는 IoC 컨테이너가 조립한 Bean인 경우가 많습니다. 웹 계층도 코어 컨테이너와 따로 떨어져 있지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Data Access&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC, 트랜잭션, ORM 연동처럼 영속성 계층을 구현할 때 반복되는 기반 작업을 지원합니다. 예외 변환과 선언적 트랜잭션을 사용하면 비즈니스 코드가 연결 관리나 커밋&amp;middot;롤백 같은 세부 구현에 덜 묶이게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Spring Framework의 데이터 액세스 지원과 별도 프로젝트인 Spring Data를 구분할 필요가 있습니다. 둘은 관련되어 있지만 같은 범위를 뜻하지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Testing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트 자체는 스프링 없이도 작성할 수 있습니다. 오히려 생성자 주입을 잘 사용한 객체는 컨테이너 없이 직접 생성해서 테스트하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 여기에 TestContext Framework, MockMvc 같은 통합 테스트 도구를 제공합니다. 애플리케이션 컨텍스트 구성, 트랜잭션 테스트, MVC 요청 매핑 등을 실제 스프링 동작과 가깝게 검증할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IoC는 무엇이 뒤집힌 것일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoC는 Inversion of Control, 제어의 역전을 뜻합니다. 이름만 보면 애플리케이션의 모든 제어권을 프레임워크가 빼앗는 것처럼 들립니다. 실제로 뒤집히는 핵심은 &lt;b&gt;객체가 자신의 의존성을 직접 만들고 찾던 책임&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxdpbP/dJMcaccTHb9/Gk9A3TGvLLkRvxsBFk2QK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxdpbP/dJMcaccTHb9/Gk9A3TGvLLkRvxsBFk2QK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxdpbP/dJMcaccTHb9/Gk9A3TGvLLkRvxsBFk2QK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxdpbP%2FdJMcaccTHb9%2FGk9A3TGvLLkRvxsBFk2QK0%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;1672&quot; height=&quot;941&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;941&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 없다면 &lt;code&gt;OrderService&lt;/code&gt;가 자신에게 필요한 구현체를 직접 생성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;class OrderService {
    private final PaymentClient paymentClient = new HttpPaymentClient();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 간단하지만 &lt;code&gt;OrderService&lt;/code&gt;가 &lt;code&gt;HttpPaymentClient&lt;/code&gt;라는 구체 구현의 생성 방법까지 알고 있습니다. 구현을 바꾸거나 테스트 대역을 사용하려면 서비스 코드를 수정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성을 외부에서 받도록 바꾸면 객체의 책임이 달라집니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class OrderService {
    private final PaymentClient paymentClient;

    OrderService(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;OrderService&lt;/code&gt;는 결제 기능을 사용한다는 사실만 알면 됩니다. 어떤 구현체를 만들고 전달할지는 외부 조립자가 결정합니다. 스프링에서는 IoC 컨테이너가 설정 정보를 읽고 이 역할을 맡습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
class PaymentConfig {

    @Bean
    PaymentClient paymentClient() {
        return new HttpPaymentClient();
    }

    @Bean
    OrderService orderService(PaymentClient paymentClient) {
        return new OrderService(paymentClient);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제어의 방향은 다음처럼 바뀝니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;직접 제어
OrderService &amp;rarr; 구현체 선택 &amp;rarr; 객체 생성

IoC 적용
설정 정보 &amp;rarr; Spring Container &amp;rarr; 객체 생성과 의존성 연결 &amp;rarr; OrderService&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IoC와 DI의 관계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IoC는 더 넓은 설계 원리이고 DI, Dependency Injection은 이를 구현하는 대표적인 방법입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IoC: 객체 생성과 연결을 누가 제어하는가에 관한 원리&lt;/li&gt;
&lt;li&gt;DI: 객체가 필요한 의존성을 외부에서 전달받도록 만드는 방법&lt;/li&gt;
&lt;li&gt;Spring Container: 설정을 읽어 Bean을 생성하고 DI를 수행하는 실행 주체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 공식 문서는 DI를 IoC의 특수한 형태로 설명합니다. 객체는 생성자, 팩터리 메서드의 인자 또는 프로퍼티를 통해 필요한 의존성을 선언하고, 컨테이너가 Bean을 만들 때 이를 주입합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링이 관리하는 객체는 무엇이 달라질까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 Bean도 본질적으로는 평범한 자바 객체입니다. 차이는 생성된 이후에 생기는 특별한 능력이 아니라, 객체의 생성과 조립 과정에 컨테이너가 참여한다는 데 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 설정에 따라 다음을 관리할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체 생성 방식&lt;/li&gt;
&lt;li&gt;다른 Bean과의 의존 관계&lt;/li&gt;
&lt;li&gt;Singleton, Prototype 같은 Scope&lt;/li&gt;
&lt;li&gt;초기화와 소멸 Callback&lt;/li&gt;
&lt;li&gt;AOP Proxy와 후처리기 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 모든 객체를 Bean으로 만들어야 하는 것은 아닙니다. 값 객체나 메서드 내부에서 잠깐 사용하는 객체까지 컨테이너에 맡기면 오히려 구조가 복잡해집니다. 애플리케이션의 주요 협력 객체와 인프라 의존성을 중심으로 관리하는 것이 자연스럽습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IoC가 실무에서 주는 이점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현을 교체하기 쉬워진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 구체 클래스의 생성 방법을 모르면 운영용 구현과 테스트용 구현을 외부에서 바꿔 끼울 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트하기 쉬워진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 인자로 가짜 구현을 전달할 수 있으므로 단위 테스트에서 전체 Spring Context를 띄우지 않아도 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공통 정책을 적용할 지점이 생긴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 객체 생성 과정에 참여하므로 Scope, Lifecycle, AOP 같은 공통 정책을 일관되게 적용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 조립과 비즈니스 로직을 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 객체는 무엇을 할지에 집중하고, 어떤 구현을 사용해 전체 애플리케이션을 구성할지는 설정이 담당합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 MVC, 데이터 액세스, 테스팅 기능을 따로 외우면 스프링이 거대한 도구 상자처럼 보입니다. 하지만 중심에는 IoC 컨테이너가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;객체가 필요한 의존성을 직접 만들고 찾지 않게 하고, 컨테이너가 설정을 바탕으로 객체를 생성하고 연결하게 만드는 것.&lt;/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;이 관점이 잡히면 이후에 배우는 Bean 등록, Component Scan, 생성자 주입, Scope, AOP와 트랜잭션도 서로 떨어진 기능이 아니라 컨테이너가 객체를 관리하기 때문에 가능한 기능으로 연결됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 원리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스프링 생태계의 중심에는 Core Container가 있습니다.&lt;/li&gt;
&lt;li&gt;IoC는 객체 생성과 의존성 조립 책임의 방향을 바꿉니다.&lt;/li&gt;
&lt;li&gt;DI는 객체가 의존성을 외부에서 전달받게 하는 IoC의 구현 방식입니다.&lt;/li&gt;
&lt;li&gt;Bean은 Spring Container가 관리하는 평범한 객체입니다.&lt;/li&gt;
&lt;li&gt;컨테이너가 객체 생성에 참여하므로 Scope, Lifecycle과 AOP 같은 정책을 적용할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출처&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/overview.html&quot;&gt;Spring Framework Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/introduction.html&quot;&gt;Introduction to the Spring IoC Container and Beans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html&quot;&gt;Dependency Injection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/testing.html&quot;&gt;Spring Testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- HUMANIZE-SUMMARY
원본/윤문본 변경률: 1.7% (verify_change_rate.py 검증)
주요 변경: 해라체 종결을 -습니다/-합니다 체로 통일
자체검증: 기술 용어·코드·링크 보존, 문체 통일 확인
등급: B — 내용 변경 없이 문체만 보수적으로 조정함
--&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/스프링 교과서</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/8</guid>
      <comments>https://yosi42.tistory.com/8#entry8comment</comments>
      <pubDate>Mon, 13 Jul 2026 22:33:40 +0900</pubDate>
    </item>
    <item>
      <title>HTTP Stateless와 Keep-Alive, 세션과 JWT</title>
      <link>https://yosi42.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP의 Stateless는 요청 사이의 애플리케이션 문맥을 자동으로 기억하지 않는다는 뜻입니다. Keep-Alive는 여러 요청에서 TCP 연결을 재사용합니다. 둘은 서로 다른 계층의 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP가 기억하는 정보와 백엔드 애플리케이션이 기억할 정보는 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 양 끝의 IP와 포트, Sequence&amp;middot;ACK 번호, 송수신 버퍼와 윈도 등 바이트 전달에 필요한 연결 상태를 관리합니다. 반면 HTTP 요청을 보낸 사용자가 누구인지, 장바구니에 무엇을 담았는지, 이전 요청에서 어떤 비즈니스 로직을 실행했는지는 알지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keep-Alive로 같은 TCP 연결을 재사용해도 로그인 상태가 저절로 이어지지는 않습니다. 서버가 사용자를 식별하려면 세션 ID나 JWT 같은 인증 정보를 각 HTTP 요청에 담아야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;Stateless는 연결을 끊는다는 뜻이 아니다&quot; data-ke-size=&quot;size23&quot;&gt;Stateless는 연결을 끊는다는 뜻이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP가 Stateless라는 말은 다음 요청을 처리할 때 서버가 이전 요청의 애플리케이션 문맥을 반드시 보관할 필요는 없다는 뜻입니다. 서버가 처리하는 데 필요한 정보는 각 요청에 들어 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stateless를 매 요청마다 TCP 연결을 끊는다는 의미로 이해하면 안 됩니다. HTTP/1.1의 Keep-Alive는 한 번 맺은 TCP 연결을 여러 요청과 응답에서 재사용합니다. 연결을 새로 만드는 비용을 줄이는 전송 방식일 뿐, 로그인 사용자나 장바구니 같은 상태를 자동으로 전달하지는 않습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;HTTP 상태와 TCP 연결 상태는 다르다&quot; data-ke-size=&quot;size23&quot;&gt;HTTP 상태와 TCP 연결 상태는 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 연결은 일반적으로 출발지 IP&amp;middot;포트와 목적지 IP&amp;middot;포트의 4-tuple로 구분합니다. 연결이 만들어지면 양 끝은 Sequence&amp;middot;ACK 번호, 송수신 버퍼, 재전송 타이머, 흐름 제어와 혼잡 제어에 필요한 값 등을 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상태는 바이트를 신뢰성 있게 전달하기 위한 것입니다. TCP는 현재 연결의 사용자가 로그인했는지, 어떤 권한을 가졌는지 알지 못합니다.&lt;/p&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;rwnd: 수신자가 자신의 버퍼 여유 공간을 송신자에게 알리는 수신 윈도입니다. 느린 수신자가 감당할 수 있는 범위로 전송량을 제한합니다.&lt;/li&gt;
&lt;li&gt;cwnd: 송신자가 네트워크의 혼잡 신호를 보고 관리하는 혼잡 윈도입니다. 네트워크에 지나치게 많은 데이터를 보내지 않도록 전송량을 제한합니다.&lt;/li&gt;
&lt;li&gt;송신 측의 미확인 전송량은 대략 min(rwnd, cwnd)를 넘지 않도록 제한됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신자가 rwnd를 보내면 송신자는 이 값을 참고합니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Stateless HTTP에서 로그인 상태를 전달하는 방법&quot; data-ke-size=&quot;size23&quot;&gt;Stateless HTTP에서 로그인 상태를 전달하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 방식은 로그인에 성공하면 서버가 임의의 세션 ID를 만들고, 브라우저는 이를 보통 쿠키에 저장합니다. 다음 요청부터 브라우저가 세션 ID를 보내면 서버는 메모리나 Redis에서 세션 데이터를 조회해 사용자를 식별합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;브라우저 쿠키: session_id=abc123
서버 또는 Redis: abc123 &amp;rarr; 사용자 ID와 권한
&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;JWT 방식에서는 서버가 사용자 정보와 만료 시간 등의 Claim을 담은 토큰에 서명합니다. 클라이언트는 JWT를 쿠키나 Authorization: Bearer 헤더로 보내고, 서버는 서명과 만료 시간을 검증해 인증 객체를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;클라이언트: 서명된 JWT 보관
서버: JWT 서명&amp;middot;만료 시간&amp;middot;Claim 검증
&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;세션 ID는 일반적으로 JWT가 아니라 의미를 추측하기 어려운 임의의 값입니다. 쿠키는 정보를 저장하고 전송하는 수단이며 JWT는 토큰 형식입니다. 따라서 JWT를 쿠키에 담아 사용할 수도 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;서버가 여러 대일 때 세션을 어떻게 유지할까&quot; data-ke-size=&quot;size23&quot;&gt;서버가 여러 대일 때 세션을 어떻게 유지할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서 뒤에 서버 A와 B가 있고 A의 JVM 메모리에만 세션을 저장했다고 가정해 보겠습니다. A에서 로그인한 사용자의 다음 요청이 B로 가면 B는 세션을 찾지 못해 사용자를 미인증 상태로 판단합니다.&lt;/p&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;h4 data-heading=&quot;Sticky Session&quot; data-ke-size=&quot;size20&quot;&gt;Sticky Session&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서가 같은 사용자의 요청을 계속 같은 서버로 보냅니다. 로컬 메모리 세션을 그대로 활용할 수 있지만 특정 서버로 부하가 쏠릴 수 있습니다. 서버가 죽으면 그 서버의 세션도 함께 사라질 수 있다는 한계도 있습니다.&lt;/p&gt;
&lt;h4 data-heading=&quot;공용 Redis 세션&quot; data-ke-size=&quot;size20&quot;&gt;공용 Redis 세션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 애플리케이션 서버가 같은 Redis에서 세션을 조회합니다. 어느 서버가 요청을 받아도 인증할 수 있어 수평 확장에 유리합니다. 다만 Redis가 인증 경로의 공용 의존성이자 병목이 되므로 복제, 장애조치, 타임아웃과 가용성을 함께 설계해야 합니다.&lt;/p&gt;
&lt;h4 data-heading=&quot;JWT&quot; data-ke-size=&quot;size20&quot;&gt;JWT&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버가 토큰의 서명을 검증하므로 사용자별 세션을 공유하지 않고 요청을 처리합니다. 애플리케이션 서버를 늘리기는 쉽지만 토큰 탈취, 강제 로그아웃, 권한 변경의 즉시 반영, Refresh Token 관리가 까다롭습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT가 언제나 세션보다 가벼운 것은 아닙니다. 세션 저장소 조회를 줄이는 대신 JWT는 세션 ID보다 크고, 요청마다 서명을 검증해야 합니다. 저장소 의존성을 줄이는 대신 전송 크기와 폐기 정책의 복잡성을 떠안는 셈입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;인증 실패와 인증 인프라 장애를 구분해야 한다&quot; data-ke-size=&quot;size23&quot;&gt;인증 실패와 인증 인프라 장애를 구분해야 한다&lt;/h3&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;자격 증명이 없거나 유효하지 않음: 401 Unauthorized&lt;/li&gt;
&lt;li&gt;인증은 됐지만 필요한 권한이 없음: 403 Forbidden&lt;/li&gt;
&lt;li&gt;세션 쿠키는 있지만 Redis 장애로 검증할 수 없음: 서버 의존성 장애이므로 503 Service Unavailable을 고려합니다.&lt;/li&gt;
&lt;li&gt;서명이 정상인 Access Token이 만료됨: 리소스 서버는 401을 반환하고, 클라이언트는 유효한 Refresh Token으로 재발급을 요청할 수 있습니다.&lt;/li&gt;
&lt;li&gt;JWT 서명이 일치하지 않음: 변조된 자격 증명으로 보고 401을 반환합니다. 검증되지 않은 토큰의 Claim을 신뢰해 관련 계정을 임의로 조작하면 안 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 응답하지 않는다고 사용자를 다시 로그인시키는 건 해결책이 아닙니다. 새 로그인 세션도 Redis에 저장할 수 없기 때문입니다. 그렇다고 인증을 우회하면 보안 문제가 생깁니다. 기본적으로 Fail Closed 정책을 적용하고 저장소의 가용성을 복구해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token도 모든 JWT 오류를 해결하지는 못합니다. 정상적으로 만료된 Access Token을 갱신할 때 사용하는 수단입니다. Access Token의 서명이 잘못됐거나 Refresh Token도 만료&amp;middot;위조됐다면 인증을 거부하거나 다시 로그인해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;결론&quot; data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Stateless와 TCP Keep-Alive는 모순되지 않습니다. Keep-Alive는 전송 계층의 연결을 재사용하고, Stateless는 애플리케이션 요청 사이의 문맥을 자동으로 보존하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드가 로그인 상태를 제공하려면 세션이나 JWT로 요청마다 사용자를 식별해야 합니다. 서버가 여러 대라면 Sticky Session, 공용 Redis, JWT의 장애 특성과 확장성을 비교해야 합니다. 인증 실패와 인증 시스템 장애를 구분해야 401, 403, 503도 상황에 맞게 선택할 수 있습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;핵심 원리&quot; data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Stateless는 애플리케이션 요청 문맥에 관한 말이지 TCP 연결 수명에 관한 말이 아닙니다.&lt;/li&gt;
&lt;li&gt;TCP 연결 상태와 로그인&amp;middot;장바구니 같은 비즈니스 상태를 구분합니다.&lt;/li&gt;
&lt;li&gt;쿠키는 전달 수단이고 세션 ID와 JWT는 사용자를 식별하는 서로 다른 방식입니다.&lt;/li&gt;
&lt;li&gt;분산 환경에서는 상태의 저장 위치가 확장성과 장애 범위를 결정합니다.&lt;/li&gt;
&lt;li&gt;인증 실패와 인증 인프라 장애를 구분해 상태 코드를 선택합니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>CS/네트워크</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/7</guid>
      <comments>https://yosi42.tistory.com/7#entry7comment</comments>
      <pubDate>Mon, 13 Jul 2026 21:33:02 +0900</pubDate>
    </item>
    <item>
      <title>[운영] 모니터링, 옵저버빌리티</title>
      <link>https://yosi42.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 성능 개선에 관심이 생겼다. 성능 개선을 하기 위해서 CPU 자원을 얼마나 사용 중인지 어디서 병목현상이 발생했는지 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그것을 알기 위한 모니터링, 옵저버빌리티의 개념과&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;메트릭을 수집할 수 있는 Micrometer에 대해서 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 옵저버빌리티(Observability)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 모니터링과 옵저버빌리티&lt;/h3&gt;
&lt;pre id=&quot;code_1783698102620&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 모니터링 (Monitoring)
	&amp;bull; 초점: 시스템에 이상이 생겼는가? (What)
	&amp;bull; 대상: 미리 예측 가능한 영역의 문제 (Known Unknowns)
	&amp;bull; 동작: 대시보드에 미리 설정해 둔 수치(예: CPU &amp;gt; 80%, 500 에러 발생률 &amp;gt; 5%)를 감시하고, 
    	   기준을 벗어나면 시스템이 개발자에게 알람을 전송합니다. 수동적이고 상태 중심적입니다.
2. 옵저버빌리티 (Observability, 관측성)
	&amp;bull; 초점: 왜 그런 이상 현상이 발생했는가? (Why)
	&amp;bull; 대상: 사전에 전혀 예측하지 못한 비정상적인 동작이나 장애 (Unknown Unknowns)
	&amp;bull; 동작: 시스템 내부의 유기적인 데이터 흐름을 통해 외부에서 내부의 상세 상태를 추론하는 시스템의 속성입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링은 &lt;b&gt;&quot;시스템에 문제가 생겼는가?(What)&quot;&lt;/b&gt;를 아는 것이고, 옵저버빌리티(Observability, 관측성)는 &lt;b&gt;&quot;그 문제가 왜 발생했는가?(Why)&quot;&lt;/b&gt;를 파악할 수 있는 시스템의 능력이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링은 &quot;무엇을 볼지&quot; 미리 정한다. 하지만 옵저버빌리티는 시스템의 모든 상태를 수집해 두고, 문제가 발생했을 때 &quot;왜&quot;를 추적할 수 있게 한다.&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;1.2 옵저버빌리티의 3가지 축&lt;/h3&gt;
&lt;pre id=&quot;code_1783698309631&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                    Observability                        │
│                                                         │
│   ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│   │   Metrics   │  │    Logs     │  │   Traces    │     │
│   │   (메트릭)    │  │   (로그)     │  │  (트레이스)   │	  │
│   └─────────────┘  └─────────────┘  └─────────────┘     │
│         │                │                │             │
│    	&quot;무엇이&quot;         &quot;무슨 일이&quot;          &quot;어디서&quot;          │
│    	&quot;얼마나&quot;         &quot;일어났는가&quot;        &quot;어떤 경로로&quot;    	  │
│                                                         │
└─────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;color: #222939; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;축&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;도구 예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Metrics&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;수치화된 시계열 데이터&lt;/td&gt;
&lt;td&gt;Prometheus, Datadog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Logs&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;이벤트 기록&lt;/td&gt;
&lt;td&gt;ELK Stack, Loki&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Traces&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;요청 흐름 추적&lt;/td&gt;
&lt;td&gt;Jaeger, Zipkin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #222939; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #222939; text-align: left;&quot; 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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Metrics&lt;/b&gt;: 시스템의 상태나 성능을 &lt;b&gt;숫자로 요약하여 집계&lt;/b&gt;한 데이터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시: &lt;span&gt;&quot;&lt;/span&gt;현재&lt;span&gt; &lt;/span&gt;웹&lt;span&gt; &lt;/span&gt;서버의&lt;span&gt; CPU &lt;/span&gt;사용률이&lt;span&gt; 85%&lt;/span&gt;이고&lt;span&gt;, &lt;/span&gt;초당&lt;span&gt; 500&lt;/span&gt;건의&lt;span&gt; &lt;/span&gt;요청을&lt;span&gt; &lt;/span&gt;처리하고&lt;span&gt; &lt;/span&gt;있습니다&lt;span&gt;.&quot; (문제 발생 감지)&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Logs&lt;/b&gt;: 특정 시간에 발생한 &lt;b&gt;개별 이벤트의 상세 내용&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&gt;예시: &lt;/span&gt;&quot;2026-07-11 00:10:05 [ERROR] &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;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;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;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;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;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;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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;(Timeout)&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;가&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; &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;발생했습니다&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;.&quot; (상세 내용)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;Traces&lt;/b&gt;: 하나의 요청이 여러 서비스나 시스템을 거쳐가는 &lt;b&gt;전체 이동 경로와 소요 시간&lt;/b&gt;을 추적한 데이터&lt;/span&gt;&lt;span&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;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예시: &lt;/span&gt;&quot;&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;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;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;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;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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; API &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;게이트웨이&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;(10ms)&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;를&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; &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;거쳐&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; &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;인증&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; &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;서비스&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;(50ms)&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;와&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; &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;결제&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; &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;서비스&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;(200ms)&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;를&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; &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;순차적으로&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; &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;통과하여&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; &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;총&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; 260ms &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;만에&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; &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;완료되었습니다&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;.&quot; (병목 지점)&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;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&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;1.3 실제 장애 대응&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1783698765271&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;장애 발생 시나리오:
오전 10:05 - 사용자 &quot;결제가 안 돼요&quot; 신고

옵저버빌리티 없이:
├── &quot;로그 파일 어디있지?&quot;
├── &quot;어떤 서버에서 발생했지?&quot;
├── &quot;재현이 안 되는데...&quot;
└── 3시간 후 원인 파악

옵저버빌리티 있으면:
├── 메트릭: 10:03부터 결제 API 지연 급증
├── 로그: PaymentService에서 timeout 에러
├── 트레이스: 외부 PG사 API 응답 5초
└── 15분 만에 원인 파악&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Prometheus &amp;amp; Micrometer&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 Prometheus&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;1378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AWyce/dJMcaglRo46/kWiemyTCkUPMG5xnL7gI1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AWyce/dJMcaglRo46/kWiemyTCkUPMG5xnL7gI1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AWyce/dJMcaglRo46/kWiemyTCkUPMG5xnL7gI1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAWyce%2FdJMcaglRo46%2FkWiemyTCkUPMG5xnL7gI1K%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;2560&quot; height=&quot;1378&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;1378&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 data-ke-size=&quot;size16&quot;&gt;- Pull 방식: Prometheus가 앱에서 메트릭을 가져감&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;- PromQL: 강력한 쿼리 언어&lt;/p&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;Pull 방식의 장점:&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;수집의 주도권을 중앙 서버가 가지고 있음 (안정성 up, 결합도 down)&lt;/li&gt;
&lt;li&gt;애플리케이션은 메트릭을 노출만 하면 됨&lt;/li&gt;
&lt;li&gt;Prometheus가 중앙에서 스크래핑 대상 관리&lt;/li&gt;
&lt;li&gt;애플리케이션의 독립성 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Micrometer&lt;/h3&gt;
&lt;pre id=&quot;code_1783699838611&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Micrometer = 메트릭의 SLF4J (추상화 계층)

┌─────────────────────────────────────────────────────────┐
│                     Application Code                    │
│                                                         │
│              meterRegistry.counter(&quot;orders&quot;).increment()│
│                              │                          │
│                              ▼                          │
│                    ┌─────────────────┐                  │
│                    │   Micrometer    │                  │
│                    │  (추상화 계층)     │                  │
│                    └────────┬────────┘                  │
│                             │                           │
│          ┌──────────────────┼──────────────────┐        │
│          ▼                  ▼                  ▼        │
│     ┌────────────┐    ┌────────────┐    ┌────────────┐  │
│     │ Prometheus │    │  Datadog   │    │ CloudWatch │  │
│     │  Registry  │    │  Registry  │    │  Registry  │  │
│     └────────────┘    └────────────┘    └────────────┘  │
└─────────────────────────────────────────────────────────┘&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;로깅을 할 때 특정 프레임워크(Logback 등)에 종속되지 않기 위해 SLF4J 같은 공통 인터페이스를 사용하는 것과 같은 역할을 '메트릭 수집' 영역에서 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 모니터링 툴에 종속되지 않는 유연성 (Vendor-neutral, 벤더 중립적)&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;3. Spring Boot Actuator와 자동 통합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 프로메테우스 pull 방식과 연계 좋음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 다양한 측정 도구 제공 (Counter, Gauge, Timer, Distribution Summary)&lt;/p&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 title=&quot;micrometer 공식 문서&quot; href=&quot;https://docs.micrometer.io/micrometer/reference/reference/jvm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;micrometer 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 메트릭 유형&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 4가지 기본 메트릭 타입&lt;/h3&gt;
&lt;pre id=&quot;code_1783700453078&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│  1. Counter (카운터)                                      │
│     - 증가만 가능 (감소 불가)                           	  │
│     - 재시작 시 0으로 리셋                              	  │
│     - 예: 총 요청 수, 총 에러 수                        	  │
│                                                         │
│     0 &amp;rarr; 1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4 &amp;rarr; 5 &amp;rarr; ...                         │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  2. Gauge (게이지)                                        │
│     - 증가/감소 모두 가능                                    │
│     - 현재 상태를 나타냄                                     │
│     - 예: 현재 메모리 사용량, 활성 스레드 수             	  │
│                                                         │
│     50 &amp;rarr; 70 &amp;rarr; 45 &amp;rarr; 80 &amp;rarr; 30 &amp;rarr; ...                        │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  3. Timer (타이머)                                        │
│     - 이벤트 지속 시간 + 발생 횟수                            │
│     - 예: API 응답 시간, 쿼리 실행 시간                       │
│                                                         │
│     count: 100, sum: 5.2s, max: 0.5s                    │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  4. Histogram (히스토그램)                                 │
│     - 값의 분포를 버킷으로 측정                               │
│     - 예: 응답 시간 분포 (0-100ms: 50%, 100-500ms: 40%) 	  │
│                                                         │
│     bucket_0.1: 50, bucket_0.5: 90, bucket_1.0: 98      │
└─────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;color: #222939; text-align: left; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;측정 대상&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;메트릭 타입&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;누적 개수&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;주문 수, 에러 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;현재 상태&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;메모리 사용량, 활성 연결 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소요 시간&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;API 응답 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;값 분포&lt;/td&gt;
&lt;td&gt;Histogram&lt;/td&gt;
&lt;td&gt;응답 시간 백분위&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Counter (카운터) : 오직 증가만 하는 누적 지표&lt;/b&gt;&lt;br /&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;핵심 특징:&lt;/b&gt; 절대 줄어들지 않으며, 서버가 재시작될 때만 0으로 초기화된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 방법:&lt;/b&gt; 모니터링 시스템(예: 프로메테우스)에서 이 누적 데이터를 가져가 초당 발생률(Rate)이나 특정 구간의 증가폭을 계산하는 데 주로 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의점:&lt;/b&gt; 스레드 풀의 활성 스레드 수처럼 값이 올라갔다 내려갈 수 있는 지표에는 절대 사용하면 안 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실무 예시:&lt;/b&gt; 누적 API 호출 횟수, 누적 주문 생성 건수, 예외(Exception) 발생 총합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Gauge (게이지) : 특정 시점의 현재 상태&lt;/b&gt;&lt;br /&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;핵심 특징:&lt;/b&gt; 데이터를 수집하는 바로 그 찰나의 값을 보여준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 방법:&lt;/b&gt; 지속적으로 변동하는 시스템의 리소스 상태나 대기열의 크기를 파악할 때 유용하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의점:&lt;/b&gt; 측정하는 순간의 값만 기록하므로, 메서드 실행 시간처럼 아주 짧은 찰나에 발생하는 이벤트를 기록하는 데는 적합하지 않다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실무 예시:&lt;/b&gt; 현재 활성화된 DB 커넥션 수, 큐(Queue)에 쌓여있는 대기 메시지 수, 현재 JVM 메모리 사용량&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Timer (타이머) : 시간과 빈도를 동시에 측정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이머는 짧은 시간 동안 발생하는 이벤트의 '지연 시간(Latency)'과 '발생 빈도(Frequency)'를 동시에 측정하기 위한 타입이다.&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;핵심 특징:&lt;/b&gt; 타이머를 하나만 세팅해도 내부적으로 이벤트 발생 횟수(Count), 총 소요 시간(Total Time), 최대 소요 시간(Max)을 한 번에 수집해 준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 방법:&lt;/b&gt; 애플리케이션의 성능 병목을 찾거나 API 응답 속도를 모니터링할 때 필수적으로 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의점:&lt;/b&gt; 백그라운드 배치 작업처럼 실행 시간이 매우 긴 작업을 측정할 때는 일반 Timer가 아닌 LongTaskTimer라는 전용 타입을 사용해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실무 예시:&lt;/b&gt; HTTP API 엔드포인트별 응답 시간, JPA DB 쿼리 실행 소요 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Distribution Summary (분포 요약) : 데이터 크기와 양의 분포&lt;/b&gt;&lt;br /&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;핵심 특징:&lt;/b&gt; 타이머처럼 발생 횟수, 총합, 최댓값을 제공하며, 데이터가 어떤 형태로 분포되어 있는지(예: 상위 95%의 크기)를 추적한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활용 방법:&lt;/b&gt; 단순 평균값으로는 파악하기 힘든 데이터 사이즈의 편차나 패턴을 분석하고 싶을 때 유용하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실무 예시:&lt;/b&gt; 사용자 요청(Request Payload)의 바이트(Byte) 크기 분포, 한 번에 업로드되는 파일 사이즈 통계, DB에서 한 번에 조회해 오는 레코드 건수&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Counter, Gauge 선택 기준&lt;/h3&gt;
&lt;pre id=&quot;code_1783700752763&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Counter를 써야 할 때:
- &quot;지금까지 총 몇 건?&quot; &amp;rarr; 주문 수, 에러 수
- 절대 감소하지 않는 값
- rate() 함수로 &quot;초당 증가율&quot; 계산 가능

Gauge를 써야 할 때:
- &quot;지금 현재 몇 개?&quot; &amp;rarr; 활성 연결 수, 큐 크기
- 증가/감소가 모두 가능한 값
- 순간 상태가 중요한 값&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/6</guid>
      <comments>https://yosi42.tistory.com/6#entry6comment</comments>
      <pubDate>Sat, 11 Jul 2026 01:45:33 +0900</pubDate>
    </item>
    <item>
      <title>HTTP HOL Blocking에서 QUIC Stream까지</title>
      <link>https://yosi42.tistory.com/2</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;HTTP/2는 하나의 TCP 연결에 여러 HTTP Stream을 Multiplexing해 HTTP 계층의 순차 대기를 줄였고, HTTP/3는 독립된 QUIC Stream을 사용해 한 Stream의 패킷 유실이 다른 Stream의 전달까지 막는 TCP 계층 HOL Blocking을 줄였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream은 하나의 연결 안에 만드는 논리적인 바이트 통로다. HTTP/2는 여러 통로처럼 보이지만 밑에서는 하나의 TCP 통로를 함께 사용한다. 그래서 TCP 데이터가 유실되면 모든 Stream이 기다릴 수 있다. HTTP/3는 QUIC이 Stream별 통로를 직접 관리하기 때문에 한 Stream의 유실이 다른 Stream의 순서까지 막지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 연결 방향마다 하나의 바이트 순서 공간을 사용한다. 중간 바이트가 유실되면 뒤에 다른 HTTP Stream의 데이터가 도착해도 소켓은 빠진 부분 이후의 바이트를 애플리케이션에 먼저 전달할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC은 Packet Number로 전송과 유실을 추적하고, Stream ID와 Stream Offset으로 데이터가 어느 Stream의 몇 번째 바이트인지 구분한다. 한 Stream에 빈 구간이 생겨도 다른 Stream의 연속된 데이터는 애플리케이션에 전달할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;HTTP/1.1은 왜 여러 TCP 연결을 사용했을까&quot; data-ke-size=&quot;size23&quot;&gt;HTTP/1.1은 왜 여러 TCP 연결을 사용했을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1은 Keep-Alive를 이용해 하나의 TCP 연결을 여러 요청에서 재사용할 수 있다. 하지만 일반적인 순차 요청에서는 앞 요청의 응답을 기다리는 동안 다음 요청을 처리하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pipelining을 사용하면 응답을 기다리지 않고 여러 요청을 먼저 보낼 수 있다. 그러나 HTTP/1.1 서버는 Pipelining 요청의 응답을 요청 순서대로 보내야 한다. 첫 번째 요청의 처리가 오래 걸리면 뒤에서 이미 끝난 응답도 먼저 보낼 수 없다. 이것이 HTTP 계층의 HOL, Head-of-Line Blocking이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 이 문제를 완화하려고 같은 서버에 여러 TCP 연결을 병렬로 열었다. 연결 개수는 표준이 고정하지 않으며 구현에 따라 달라진다. 여러 연결은 병렬성을 얻지만 TCP&amp;middot;TLS 연결 비용과 서버 자원 사용량도 늘린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;HTTP/2 Multiplexing&quot; data-ke-size=&quot;size23&quot;&gt;HTTP/2 Multiplexing&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2는 HTTP 메시지를 작은 Frame으로 나누고 Stream ID를 붙인다. 여러 Stream의 Frame을 하나의 TCP 연결에 교차 배치할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;하나의 HTTP/2 TCP 연결

HEADERS(Stream 1)
HEADERS(Stream 3)
DATA(Stream 3)
DATA(Stream 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;Stream 1의 애플리케이션 처리가 느리더라도 준비된 Stream 3의 Frame을 먼저 보낼 수 있다. HTTP/1.1의 응답 순서 제약에서 발생한 애플리케이션 계층 HOL Blocking을 줄이는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;HTTP/2에도 TCP HOL Blocking은 남는다&quot; data-ke-size=&quot;size23&quot;&gt;HTTP/2에도 TCP HOL Blocking이 발생한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2의 여러 Stream은 결국 하나의 TCP 바이트 스트림에 실린다. TCP는 애플리케이션에 순서대로 이어진 바이트만 전달한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;TCP 바이트 구간 A: Stream 1 데이터 &amp;mdash; 유실
TCP 바이트 구간 B: Stream 3 데이터 &amp;mdash; 도착
&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;수신 TCP가 B를 버리지 않고 버퍼에 저장하더라도, A가 재전송되기 전에는 B를 HTTP/2 계층에 전달할 수 없다. Stream 1의 손실이 Stream 3까지 멈추게 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;QUIC Stream이란 무엇인가&quot; data-ke-size=&quot;size23&quot;&gt;QUIC Stream이란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC 연결 안에는 여러 개의 독립적인 Stream이 존재한다. Stream은 애플리케이션에 순서 있는 바이트 통로를 제공한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;QUIC Connection
├── Stream 0: 요청 A의 순서 있는 바이트
├── Stream 4: 요청 B의 순서 있는 바이트
└── Stream 8: 요청 C의 순서 있는 바이트
&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;HTTP/3에서는 일반적으로 하나의 요청&amp;middot;응답 쌍이 하나의 양방향 QUIC Stream을 사용한다. 같은 Stream 안에서는 데이터 순서를 보장하지만, 서로 다른 Stream 사이에는 하나의 전체 전달 순서를 강제하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Stream 0의 중간 데이터가 유실돼도 Stream 4의 데이터가 연속적으로 도착했다면 Stream 4는 계속 처리할 수 있다. 이것이 HTTP/3가 TCP 계층 HOL Blocking을 줄이는 핵심이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Packet Number와 Stream Offset&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-heading=&quot;Packet Number와 Stream Offset&quot; data-ke-size=&quot;size23&quot;&gt;Packet Number와 Stream Offset&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC은 전송 순서와 데이터 전달 순서를 서로 다른 값으로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 관리 대상 역할&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;Packet Number&lt;/td&gt;
&lt;td&gt;QUIC 패킷&lt;/td&gt;
&lt;td&gt;어떤 전송이 ACK됐거나 유실됐는지 추적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream ID&lt;/td&gt;
&lt;td&gt;논리적 Stream&lt;/td&gt;
&lt;td&gt;데이터가 어느 Stream에 속하는지 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream Offset&lt;/td&gt;
&lt;td&gt;Stream 내부 바이트&lt;/td&gt;
&lt;td&gt;해당 데이터가 Stream의 몇 번째 위치인지 표시&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;TCP Sequence Number는 바이트 스트림의 위치를 나타내며 같은 바이트를 재전송할 때 같은 Sequence 범위를 사용한다. 반면 QUIC Packet Number는 전송 자체를 식별한다. 유실된 Stream 데이터를 다시 보낼 때는 같은 Stream Offset의 데이터를 새로운 Packet Number를 가진 패킷에 담는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Packet 100: Stream 0, Offset 0~999   &amp;mdash; 유실
Packet 101: Stream 4, Offset 0~999   &amp;mdash; 도착
Packet 102: Stream 0, Offset 0~999   &amp;mdash; 새 Packet Number로 재전송
&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;Packet Number는 ACK와 유실 감지에 사용하고, Stream Offset은 재전송된 데이터까지 포함해 애플리케이션 바이트 순서를 복원하는 데 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;QUIC은 UDP가 하지 않는 일을 직접 제공한다&quot; data-ke-size=&quot;size23&quot;&gt;QUIC은 UDP가 하지 않는 일을 직접 제공한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 신뢰성, 순서, 재전송, 흐름 제어와 혼잡 제어를 제공하지 않는다. QUIC은 UDP 위에서 동작하지만 다음 전송 기능을 직접 구현한다.&lt;/p&gt;
&lt;h4 data-heading=&quot;패킷 유실 감지와 재전송&quot; data-ke-size=&quot;size20&quot;&gt;1. 패킷 유실 감지와 재전송&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신자는 ACK 범위로 받은 Packet Number를 알린다. 송신자는 더 나중의 패킷이 ACK됐는데 앞 패킷이 일정 패킷 수 또는 시간 이상 ACK되지 않으면 유실로 판단한다. 응답이 오지 않을 때는 PTO, Probe Timeout을 이용해 복구를 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유실된 패킷 자체를 같은 번호로 다시 보내는 것이 아니라, 필요한 Frame 데이터를 새로운 Packet Number의 패킷에 다시 담는다.&lt;/p&gt;
&lt;h4 data-heading=&quot;Stream과 Connection 흐름 제어&quot; data-ke-size=&quot;size20&quot;&gt;2. Stream과 Connection 흐름 제어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC은 두 단계로 수신자의 처리 능력을 보호한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAX_STREAM_DATA: 특정 Stream이 보낼 수 있는 최대 바이트 위치를 알린다.&lt;/li&gt;
&lt;li&gt;MAX_DATA: 연결의 모든 Stream을 합쳐 보낼 수 있는 전체 데이터 한도를 알린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Stream이 수신 버퍼를 독점하지 못하도록 Stream별 한도와 연결 전체 한도를 함께 사용한다.&lt;/p&gt;
&lt;h4 data-heading=&quot;연결 전체 혼잡 제어&quot; data-ke-size=&quot;size20&quot;&gt;3. 연결 전체 혼잡 제어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream의 전달 순서는 독립적이어도 네트워크 경로의 대역폭은 공유한다. QUIC 혼잡 제어는 일반적으로 연결의 경로 단위로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RFC 9002는 TCP NewReno와 비슷한 혼잡 제어를 예시로 정의한다. 혼잡 회피에서는 AIMD 방식으로 윈도를 늘리고 손실이나 ECN 혼잡 신호가 발생하면 줄인다. 다만 QUIC이 NewReno만 강제하는 것은 아니며 CUBIC 같은 다른 송신 측 알고리즘도 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 HTTP/3가 한 Stream의 순서 대기를 다른 Stream과 분리해도, 패킷 손실로 연결 전체의 혼잡 윈도가 감소하면 다른 Stream의 처리율도 영향을 받을 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-heading=&quot;결론&quot; data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1의 Pipelining은 응답 순서 때문에 HOL Blocking이 발생한다. HTTP/2는 하나의 TCP 연결에서 여러 HTTP Stream을 Multiplexing해 이 애플리케이션 계층 제약을 줄였다. 하지만 TCP가 연결 전체를 하나의 순서 있는 바이트 스트림으로 제공하기 때문에 패킷 유실 시 모든 HTTP/2 Stream이 기다릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/3의 QUIC은 Packet Number로 전송을 추적하고 Stream ID와 Stream Offset으로 전달 순서를 Stream별로 분리한다. 그래서 한 Stream의 유실이 다른 Stream의 데이터 전달까지 직접 막지 않는다. 다만 흐름 제어와 혼잡 제어 자원은 연결과 경로 수준에서도 공유되므로 모든 성능 문제가 사라지는 것은 아니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-heading=&quot;핵심 원리&quot; data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HOL Blocking이 어느 계층에서 어떻게 생기는지 구분한다.&lt;/li&gt;
&lt;li&gt;Multiplexing은 여러 논리적 작업을 하나의 연결에 교차 배치하는 것이다.&lt;/li&gt;
&lt;li&gt;TCP는 연결 전체의 전달 순서를, QUIC은 Stream별 전달 순서를 제공한다.&lt;/li&gt;
&lt;li&gt;QUIC Packet Number는 전송 순서, Stream Offset은 애플리케이션 데이터 순서를 나타낸다.&lt;/li&gt;
&lt;li&gt;Stream 독립성과 네트워크 혼잡 자원 공유는 동시에 존재한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>CS/네트워크</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/2</guid>
      <comments>https://yosi42.tistory.com/2#entry2comment</comments>
      <pubDate>Mon, 29 Jun 2026 21:07:30 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 자바 21 가상 스레드</title>
      <link>https://yosi42.tistory.com/1</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;비동기와 동기에 대해서 공부를 하던 중 2023.09.19부터 JDK 21 LTS는 &lt;/span&gt;&lt;b&gt;가상 스레드&lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; font-size: 16px; 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: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 &lt;b&gt;Spring&lt;/b&gt;은 2023.11.17 부터, &lt;b&gt;Spring Boot&lt;/b&gt; 3.2는 2023.11.23 부터 JDK 21을 지원하고 있다.&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: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;가상 스레드는 경량 스레드이다. OS 스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해서 &lt;b&gt;수십만~수백만 개의&lt;/b&gt; 스레드를 동시에 사용할 수 있게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러면 가상 스레드 도입 전에 Java는 어떻게 스레드를 관리하고 있었을까?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 스레드는 OS Thread를 Wrapping 한 것이다. 이것을 Platform Thread라고 부른다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Wrapping: low-level의 시스템 자원을 개발자가 코드에서 쉽게 사용할 수 있도록 자바 객체로 포장해서 제공하는 것&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS Thread는 운영체제가 알고 있는 스레드이다. 그래서 생성 개수가 제한적이고 생성, 유지하는 비용이 비싸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 애플리케이션에서 플랫폼 스레드를 효율적으로 사용하기 위해서 Thread Pool을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Thread Pool&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread Pool은 스레드를 매번 새로 만들고 없애기엔 비용이 너무 비싸서, 미리 일정 개수를 만들어두고 작업이 들어올 때마다 배정해서 돌려쓰는 스레드 5분 대기조이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1VUW9/dJMcacDG1HS/YjI0v0NAWmfDZLHHYITSKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1VUW9/dJMcacDG1HS/YjI0v0NAWmfDZLHHYITSKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1VUW9/dJMcacDG1HS/YjI0v0NAWmfDZLHHYITSKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1VUW9%2FdJMcacDG1HS%2FYjI0v0NAWmfDZLHHYITSKk%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;713&quot; height=&quot;121&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;987&quot; data-origin-height=&quot;167&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;기본적인 spring mvc 에서 Thread Per Request 방식으로 하나의 요청 당 하나의 스레드를 할당했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 OS Thread를 무한정 늘릴 수 없기 때문에 처리량이 제한되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 IO Bound의 작업을 처리 중이면 스레드가 실제로 일하는 시간보다 대기하는 시간이 길어지게 된다.&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;Reactive Programming (Webflux)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bigAn2/dJMcaglIBFr/1K6LR2VStM27aQmfOSKfL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bigAn2/dJMcaglIBFr/1K6LR2VStM27aQmfOSKfL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bigAn2/dJMcaglIBFr/1K6LR2VStM27aQmfOSKfL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbigAn2%2FdJMcaglIBFr%2F1K6LR2VStM27aQmfOSKfL0%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;540&quot; height=&quot;255&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;430&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;Webflux는 비동기와 논블로킹 처리를 지원하기 위해 만들어진 리액티브 웹 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 IO작업을 요청하고 결과가 도착할 때까지 기다리는 것이 아니라 다른 작업을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive Programming을 사용하려면 모든 것이 논블로킹이어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 흔히 사용하는 JPA는 동기/블로킹 방식이다. 그래서 비동기를 지원하는 R2DBC 같은 ORM을 사용해야 한다.&lt;/p&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;: 자바의 디자인은 스레드 중심으로 되어있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 Exception Stack Trace, Debugger, Profiling 모두 스레드 기반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactive 할 때 작업이 여러 스레드를 걸쳐서 이루어지는데, 컨텍스트 확인이 어려워 디버깅이 어려워진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존의 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 처리량의 한계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Blocking으로 인한 리소스 낭비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Reactive Programming의 단점&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가상 스레드의 목적&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 개발자가 하드웨어 성능을 잘 활용하는 높은 처리량의 서버를 작성하는 것&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: #ef5369;&quot;&gt;가상 스레드&lt;/span&gt;는 블로킹이 발생하면 부적으로 스케줄링을 활용하여 플랫폼 스레드가 그냥 대기하게 두지 않고 다른 &lt;span style=&quot;color: #ef6f53;&quot;&gt;가상 스레드&lt;/span&gt;가 작업할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 자바 플랫폼의 디자인과 조화를 이루는 코드를 생성할 수 있도록 하는 것&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;플랫폼 스레드와 가상 스레드의 차이점&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BXTb7/dJMcaasksgx/CcuyOpHVrOWtVxaslnTkC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BXTb7/dJMcaasksgx/CcuyOpHVrOWtVxaslnTkC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BXTb7/dJMcaasksgx/CcuyOpHVrOWtVxaslnTkC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBXTb7%2FdJMcaasksgx%2FCcuyOpHVrOWtVxaslnTkC0%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;718&quot; height=&quot;365&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플랫폼 스레드는 운영체제가 직접 스케줄링하고 JVM이 스레드를 매칭할 때 Java Native Interface를 통해 커널에 Naitive Tread 생성을 요청해 자바의 스레드와 1 : 1 로 매핑하는 구조이다.&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;2410&quot; data-origin-height=&quot;1020&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6zVEb/dJMcaiqjtDG/EYS6pT1axLvDW3t80yhg31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6zVEb/dJMcaiqjtDG/EYS6pT1axLvDW3t80yhg31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6zVEb/dJMcaiqjtDG/EYS6pT1axLvDW3t80yhg31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6zVEb%2FdJMcaiqjtDG%2FEYS6pT1axLvDW3t80yhg31%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;742&quot; height=&quot;314&quot; data-origin-width=&quot;2410&quot; data-origin-height=&quot;1020&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h4 data-ke-size=&quot;size20&quot;&gt;사용하는 자원의 차이&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 24.263566%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 38.449611%; height: 18px;&quot;&gt;&lt;b&gt;플랫폼 스레드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.286822%; height: 18px;&quot;&gt;&lt;b&gt;가상 스레드&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 24.263566%; height: 18px;&quot;&gt;메타 데이터 사이즈&lt;/td&gt;
&lt;td style=&quot;width: 38.449611%; height: 18px;&quot;&gt;약 2kb&lt;/td&gt;
&lt;td style=&quot;width: 37.286822%; height: 18px;&quot;&gt;200 ~ 300 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 24.263566%; height: 18px;&quot;&gt;메모리&lt;/td&gt;
&lt;td style=&quot;width: 38.449611%; height: 18px;&quot;&gt;미리 할당된 Stack 사용&lt;/td&gt;
&lt;td style=&quot;width: 37.286822%; height: 18px;&quot;&gt;필요시 마다 Heap 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 24.263566%; height: 18px;&quot;&gt;컨텍스트 스위칭 비용&lt;/td&gt;
&lt;td style=&quot;width: 38.449611%; height: 18px;&quot;&gt;1 ~ 10us&lt;/td&gt;
&lt;td style=&quot;width: 37.286822%; height: 18px;&quot;&gt;ns (or 1us 미만)&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가상 스레드 동작 원리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2106&quot; data-origin-height=&quot;1592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mFhzt/dJMcahycz41/LOWPPU2MySMGEVP5YjD0c0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mFhzt/dJMcahycz41/LOWPPU2MySMGEVP5YjD0c0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mFhzt/dJMcahycz41/LOWPPU2MySMGEVP5YjD0c0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmFhzt%2FdJMcahycz41%2FLOWPPU2MySMGEVP5YjD0c0%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;750&quot; height=&quot;567&quot; data-origin-width=&quot;2106&quot; data-origin-height=&quot;1592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 실행될 가상 스레드 작업인 runContinuatioin을 carrier thread의 workQueue에 push 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Work queue에 있는 runContinuation들은 forkJoinPool에 의해 work stealing 방식으로 carrier thread에 의해 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 처리되던 runContinuation 들은 I/O, Sleep으로 인한 interrupt나 작업 완료 시, work queue에서 pop되어 park 과정에 의해 다시 힙 메모리로 되돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 플랫폼 스레드&lt;/b&gt;: 무거운 OS 스레드 기반으로, I/O 대기 시 자원 낭비와 처리량 한계 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리액티브 (WebFlux)&lt;/b&gt;: 성능은 올렸으나 코드가 복잡해지고 디버깅이 어려워지는 단점 존재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가상 스레드 (JDK 21)&lt;/b&gt;: JVM이 직접 스케줄링하는 초경량 스레드로, 적은 자원으로 수백만 개 생성 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 의의&lt;/b&gt;: 익숙하고 쉬운&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;동기식 코드와 디버깅 환경을 유지&lt;/b&gt;하면서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비동기 수준의 높은 처리량&lt;/b&gt;을 달성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 실제로 얼마나 좋은지 알아보자&lt;/p&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>virtual thread</category>
      <category>가상 스레드</category>
      <category>자바</category>
      <author>yosi</author>
      <guid isPermaLink="true">https://yosi42.tistory.com/1</guid>
      <comments>https://yosi42.tistory.com/1#entry1comment</comments>
      <pubDate>Mon, 29 Jun 2026 20:21:32 +0900</pubDate>
    </item>
  </channel>
</rss>