<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>xeulbn-dev 님의 블로그</title>
    <link>https://xeulbn-dev.tistory.com/</link>
    <description>xeulbn-dev 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 15 Jun 2026 12:59:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>xeulbn-dev</managingEditor>
    <item>
      <title>[CS Study] @Configuration과 @Component는 뭐가 다를까?</title>
      <link>https://xeulbn-dev.tistory.com/52</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 공부하다 보면 Bean 등록 방식에서 한 번쯤 헷갈리는 지점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Component를 붙이면 Bean으로 등록됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzBReX/dJMcaaewb5e/vVbkJaun6H7e0XyZ09c6kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzBReX/dJMcaaewb5e/vVbkJaun6H7e0XyZ09c6kk/img.png&quot; data-alt=&quot;@Configuration에서도 @Component를 확인할 수 있다?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzBReX/dJMcaaewb5e/vVbkJaun6H7e0XyZ09c6kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzBReX%2FdJMcaaewb5e%2FvVbkJaun6H7e0XyZ09c6kk%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;1364&quot; height=&quot;856&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@Configuration에서도 @Component를 확인할 수 있다?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;그런데 @Configuration도 내부적으로 @Component를 포함하고 있습니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;254&quot; data-start=&quot;237&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 이런 의문이 생깁니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;321&quot; data-start=&quot;256&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;321&quot; data-start=&quot;258&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;ldquo;어차피 둘 다 Bean으로 등록되는 거면, &lt;br /&gt;@Configuration과 @Component는 같은 거 아닌가?&amp;rdquo;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;477&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;477&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;477&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 &lt;b&gt;둘 다 Spring Bean으로 등록될 수 있지만 목적이 다릅니다.&lt;/b&gt;&lt;br /&gt;@Component는 일반 클래스를 Bean으로 등록하기 위한 어노테이션이고,&lt;/p&gt;
&lt;p data-end=&quot;477&quot; data-start=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;@Configuration은 Bean 등록 설정을 담당하는 클래스임을 나타내는 어노테이션입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-07 오후 3.35.33.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byFVKK/dJMcahED7yh/pww8KCROqIGhKQlkajqnNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byFVKK/dJMcahED7yh/pww8KCROqIGhKQlkajqnNK/img.png&quot; data-alt=&quot;출처 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byFVKK/dJMcahED7yh/pww8KCROqIGhKQlkajqnNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyFVKK%2FdJMcahED7yh%2Fpww8KCROqIGhKQlkajqnNK%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;1782&quot; height=&quot;778&quot; data-filename=&quot;스크린샷 2026-06-07 오후 3.35.33.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;683&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;683&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;Spring 공식 문서에서도 @Configuration은 하나 이상의 @Bean 메서드를 선언하는 클래스이며,&lt;/p&gt;
&lt;p data-end=&quot;683&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;Spring Container가 해당 메서드를 처리해 Bean 정의를 생성한다고 설명합니다.&lt;/p&gt;
&lt;p data-end=&quot;683&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;683&quot; data-start=&quot;479&quot; data-ke-size=&quot;size16&quot;&gt;또한 @Configuration 자체도 @Component를 포함하고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;855&quot; data-start=&quot;685&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 @Component, @Configuration, @Bean의 차이를 Bean 등록 방식과 프록시 동작 중심으로 정리해보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;855&quot; data-start=&quot;685&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  본론&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;855&quot; data-start=&quot;685&quot; data-ke-size=&quot;size20&quot;&gt;1. Spring Bean이란?&lt;/h4&gt;
&lt;p data-end=&quot;855&quot; data-start=&quot;685&quot; data-ke-size=&quot;size16&quot;&gt;먼저 Bean부터 정리해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;1063&quot; data-start=&quot;916&quot; data-ke-size=&quot;size16&quot;&gt;Spring Bean은 &lt;b&gt;Spring IoC Container가 생성하고 관리하는 객체&lt;/b&gt;입니다.&lt;br /&gt;우리가 직접 new로 객체를 생성해서 사용하는 것이 아니라, Spring Container에 객체 생성을 맡기고 필요한 곳에서 주입받아 사용하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;1092&quot; data-start=&quot;1065&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 코드가 있다고 하겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;MemberService memberService = new MemberService();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1182&quot; data-start=&quot;1158&quot; data-ke-size=&quot;size16&quot;&gt;이 코드는 개발자가 직접 객체를 생성합니다.&lt;/p&gt;
&lt;p data-end=&quot;1225&quot; data-start=&quot;1184&quot; data-ke-size=&quot;size16&quot;&gt;반면 Spring에서는 다음처럼 객체를 Bean으로 등록해두고 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component
public class MemberService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1361&quot; data-start=&quot;1282&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1361&quot; data-start=&quot;1282&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;b&gt;Spring이 애플리케이션 실행 시점에 MemberService 객체를 생성하고, IoC Container 안에서 관리&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;1361&quot; data-start=&quot;1282&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1388&quot; data-start=&quot;1363&quot; data-ke-size=&quot;size16&quot;&gt;즉, Bean 등록의 핵심은&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;객체&amp;nbsp;생성과&amp;nbsp;의존관계&amp;nbsp;관리를&amp;nbsp;개발자가&amp;nbsp;직접&amp;nbsp;하지&amp;nbsp;않고,&lt;br /&gt;Spring Container에게 맡기는 것입니다.&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;&lt;/div&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;2. Component는 자동 Bean 등록입니다.&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;/h3&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;@Component는 클래스 자체를 Spring Bean으로 등록할 때 사용합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Component
public class MemberService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1668&quot; data-start=&quot;1602&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 Spring은 컴포넌트 스캔 과정에서 @Component가 붙은 클래스를 발견하고 Bean으로 등록합니다.&lt;/p&gt;
&lt;p data-end=&quot;1668&quot; data-start=&quot;1602&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1684&quot; data-start=&quot;1670&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Component &amp;rarr; 이 클래스 자체를 Bean으로 등록해줘&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1802&quot; data-start=&quot;1734&quot; data-ke-size=&quot;size16&quot;&gt;@Service, @Repository, @Controller도 내부적으로는 @Component 계열입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
public class MemberService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public class MemberRepository {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Controller
public class MemberController {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2045&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;이 어노테이션들은 모두 컴포넌트 스캔 대상이 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;2045&quot; data-start=&quot;1975&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다만 의미를 더 명확히 하기 위해 계층별로 나누어 사용하는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;2130&quot; data-start=&quot;2047&quot; data-ke-size=&quot;size16&quot;&gt;@Service는 비즈니스 로직 계층, @Repository는 데이터 접근 계층, @Controller는 웹 요청 처리 계층을 나타냅니다.&lt;/p&gt;
&lt;p data-end=&quot;2130&quot; data-start=&quot;2047&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2371&quot; data-start=&quot;2347&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;자동 Bean 등록&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-end=&quot;2421&quot; data-start=&quot;2378&quot; data-section-id=&quot;px1v1j&quot; data-ke-size=&quot;size20&quot;&gt;3. @Configuration + @Bean은 수동 Bean 등록입니다&lt;/h4&gt;
&lt;p data-end=&quot;2457&quot; data-start=&quot;2423&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 @Configuration과 @Bean입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2678&quot; data-start=&quot;2625&quot; data-ke-size=&quot;size16&quot;&gt;여기서 @Configuration은 이 클래스가 &lt;b&gt;Bean 설정 클래스&lt;/b&gt;임을 의미합니다.&lt;/p&gt;
&lt;p data-end=&quot;2678&quot; data-start=&quot;2625&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2737&quot; data-start=&quot;2680&quot; data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;@Bean은 해당 메서드가 반환하는 객체를 Spring Bean으로 등록하겠다는 의미&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-07 오후 3.42.04.png&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O9U3o/dJMcajiblPO/sBXnaKCJXCGQB6xEkDjenK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O9U3o/dJMcajiblPO/sBXnaKCJXCGQB6xEkDjenK/img.png&quot; data-alt=&quot;출처 : https://docs.spring.io/spring-framework/reference/core/beans/java/basic-concepts.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O9U3o/dJMcajiblPO/sBXnaKCJXCGQB6xEkDjenK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO9U3o%2FdJMcajiblPO%2FsBXnaKCJXCGQB6xEkDjenK%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;2004&quot; height=&quot;528&quot; data-filename=&quot;스크린샷 2026-06-07 오후 3.42.04.png&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://docs.spring.io/spring-framework/reference/core/beans/java/basic-concepts.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;Spring 공식 문서에서도 @Bean은 메서드가 객체를 생성, 설정, 초기화하고&lt;/p&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;그 객체를 Spring IoC Container가 관리하도록 등록하는 역할을 한다고 설명합니다.&lt;/p&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;또한 @Bean 메서드는 모든 @Component 클래스 안에서도 사용할 수 있지만,&lt;/p&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;가장 일반적으로는 @Configuration 클래스와 함께 사용된다고 설명합니다.&lt;/p&gt;
&lt;p data-end=&quot;2978&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
&amp;rarr; 이 클래스는 Bean 등록 설정 클래스야

@Bean
&amp;rarr; 이 메서드가 반환하는 객체를 Bean으로 등록해줘&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;예를 들어 다음 코드에서는 두 개의 Bean이 등록됩니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3291&quot; data-start=&quot;3271&quot; data-ke-size=&quot;size16&quot;&gt;등록되는 Bean은 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. AppConfig 자체
2. memberService()가 반환한 MemberService 객체&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;3391&quot; data-start=&quot;3363&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 AppConfig 자체도 Bean이 될까요?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;3548&quot; data-start=&quot;3393&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3548&quot; data-start=&quot;3393&quot; data-ke-size=&quot;size16&quot;&gt;@Configuration이 내부적으로 @Component를 포함하고 있기 때문입니다.&lt;br /&gt;따라서 @Configuration 클래스도 컴포넌트 스캔 대상이 되고, Spring Bean으로 등록됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-end=&quot;3591&quot; data-start=&quot;3555&quot; data-section-id=&quot;hz0n50&quot; data-ke-size=&quot;size26&quot;&gt;4. @Component와 @Configuration의 차이&lt;/h2&gt;
&lt;p data-end=&quot;3631&quot; data-start=&quot;3593&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 Bean으로 등록될 수 있습니다.&lt;br /&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 style=&quot;text-align: center;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@Component&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@Configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;일반 클래스 Bean 등록&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Bean 설정 클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;등록 방식&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;클래스 자체를 Bean으로 등록&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;클래스 자체도 Bean, 내부 @Bean 메서드 결과도 Bean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;주 사용처&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Service, Repository, Controller, Utility&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;외부 라이브러리 객체 등록, 설정 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@Bean 사용&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;가능은 함&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;주로 여기서 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;핵심 차이&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;일반 컴포넌트&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@Bean 메서드 호출을 프록시로 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단순히 &amp;ldquo;Bean으로 등록된다&amp;rdquo;만 보면 둘은 비슷해 보입니다.&lt;/p&gt;
&lt;p data-end=&quot;4087&quot; data-start=&quot;3991&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Spring 입장에서 @Configuration은 단순한 컴포넌트가 아닙니다.&lt;br /&gt;@Bean 메서드를 통해 여러 Bean을 등록하는 &lt;b&gt;설정 클래스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-end=&quot;4137&quot; data-start=&quot;4094&quot; data-section-id=&quot;tfgcml&quot; data-ke-size=&quot;size26&quot;&gt;5. 중요한 차이: @Configuration은 프록시 처리가 들어갑니다&lt;/h2&gt;
&lt;p data-end=&quot;4156&quot; data-start=&quot;4139&quot; data-ke-size=&quot;size16&quot;&gt;가장 중요한 차이는 여기입니다.&lt;/p&gt;
&lt;p data-end=&quot;4171&quot; data-start=&quot;4158&quot; data-ke-size=&quot;size16&quot;&gt;다음 코드를 보겠습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4510&quot; data-start=&quot;4453&quot; data-ke-size=&quot;size16&quot;&gt;memberService() 안에서 memberRepository()를 직접 호출하고 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;4551&quot; data-start=&quot;4512&quot; data-ke-size=&quot;size16&quot;&gt;일반 Java 코드라면 메서드를 호출할 때마다 다음 코드가 실행됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;return new MemoryMemberRepository();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 호출할 때마다 새로운 객체가 만들어질 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;@Configuration을 사용하면 Spring이 설정 클래스를 프록시로 감싸서 관리&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;@Configuration 클래스 안의 @Bean 메서드가&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;같은 클래스의 다른 @Bean 메서드를 직접 호출하더라도,&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;scoping과 AOP semantics를 보장합니다.&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4634&quot; data-start=&quot;4603&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 런타임에 CGLIB subclassing이 사용됩니다.&lt;/p&gt;
&lt;p data-end=&quot;4918&quot; data-start=&quot;4898&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 다음과 같은 흐름입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;memberRepository() 호출
&amp;rarr; 이미 Spring Container에 등록된 Bean이 있는지 확인
&amp;rarr; 있으면 기존 Bean 반환
&amp;rarr; 없으면 새로 생성 후 Bean으로 등록&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;그래서 memberRepository()를 여러 번 호출하더라도 매번 새로운 객체가 만들어지는 것이 아니라,&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;Spring Container에 등록된 같은 Bean을 반환합니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 덕분에 싱글톤이 깨지지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(여기서 잠깐!) CGLIB subclassing? scoping? AOP semantics?&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) CGLIB subclassing&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;CGLIB은 Java 클래스의 &lt;b&gt;자식 클래스, 즉 subclass를 런타임에 동적으로 만들어주는 라이브러리&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 우리가 작성한 설정 클래스는 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1780828810072&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;그런데 Spring은 이 AppConfig를 그대로 사용하는 것이 아니라, 내부적으로 대략 다음과 비슷한 자식 클래스를 만들어 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780828825722&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppConfigEnhancer extends AppConfig {

    @Override
    public MemberRepository memberRepository() {
        // Spring Container에 이미 Bean이 있는지 확인
        // 있으면 기존 Bean 반환
        // 없으면 super.memberRepository() 호출 후 Bean 등록
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;물론 실제 코드는 훨씬 복잡하지만, 주요한 내용은&amp;nbsp;&lt;span&gt;Spring이 @Configuration 클래스를 상속한 프록시 객체를 만들고 &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;@Bean 메서드 호출을 가로챕니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;그래서 다음 코드에서&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1780828882282&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public MemberService memberService() {
    return new MemberService(memberRepository());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;memberRepository()를 직접 호출하는 것처럼 보이지만 실제로는 Spring이 만든 프록시 객체의 메서드가 호출됩니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;그 결과 매번 new MemoryMemberRepository()가 실행되는 것이 아니라,&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;Spring Container에 등록된 기존 Bean이 반환될 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) scoping&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;scoping은 Bean이 &lt;b&gt;어떤 범위에서 생성되고 재사용될 것인지&lt;/b&gt;를 의미합니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;Spring Bean의 기본 scope는 singleton입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780828953152&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public MemberRepository memberRepository() {
    return new MemoryMemberRepository();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;별도 설정을 하지 않으면 이 Bean은 singleton scope입니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-style=&quot;style3&quot;&gt;Singleton Scope &lt;br /&gt;= Spring Container안에서 Bean 객체가 하나만 생성되고,&lt;br /&gt;여러 곳에서 같은 객체를 공유합니다.&lt;/blockquote&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음처럼 Bean에서 memberRepository()를 사용한다고 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780829025572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public MemberService memberService() {
    return new MemberService(memberRepository());
}

@Bean
public OrderService orderService() {
    return new OrderService(memberRepository());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;일반 Java라면 memberRepository()가 두 번 호출되므로 MemoryMemberRepository 객체가 두 개 생성될 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Spring의 singleton scope에서는 하나의 Bean만 생성되어야 합니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-style=&quot;style3&quot;&gt;memberService&amp;nbsp;&amp;rarr; memberRepository Bean A 사용&lt;br /&gt;orderService &amp;rarr; memberRepsoitory Bean A 사용&lt;/blockquote&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring은 @Bean 메서드를 여러 번 호출해도 singleton Bean이라면 같은 객체가 반환되는 것을 보장합니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;물론 Spring에는 Singleton만 있는 것은 아닙니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780829183489&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;singleton  : Spring Container 안에서 하나만 생성
prototype  : 요청할 때마다 새로 생성
request    : HTTP 요청마다 하나 생성
session    : HTTP Session마다 하나 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;따라서 scoping을 보장한다는 말은 단순히 singleton만을 의미하는 것은 아니며,&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;Bean에 설정된 scope 규칙대로 객체 생명주기를 지켜준다는 의미입니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) AOP semantics&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &lt;b&gt;Spring AOP가 적용된 Bean의 동작 방식이 깨지지 않도록 보장한다는 뜻&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;AOP는 공통 관심사를 핵심 로직과 분리하는 방식입니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 기능들이 AOP로 처리될 수 있습니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-style=&quot;style3&quot;&gt;@Transactional &lt;br /&gt;@Cacheable &lt;br /&gt;@Async &lt;br /&gt;로깅 &lt;br /&gt;권한 검사 &lt;br /&gt;성능 측정&lt;/blockquote&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 많이 알고 계신 것이 바로 @Transactional입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780829305566&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MemberService {

    @Transactional
    public void join() {
        // 회원가입 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;@Transactional이 붙으면 Spring은 MemberService를 그대로 사용하는 것이 아니라&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션 처리를 위한 프록시 객체를 만들어 사용합니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;흐름은 대략 아래와 같습니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-style=&quot;style3&quot;&gt;memberService.join() 호출 &lt;br /&gt;&amp;rarr; 프록시가 먼저 호출됨 &lt;br /&gt;&amp;rarr; 트랜잭션 시작 &lt;br /&gt;&amp;rarr; 실제 join() 실행 &lt;br /&gt;&amp;rarr; 성공하면 commit &lt;br /&gt;&amp;rarr; 실패하면 rollback&lt;/blockquote&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Bean을 사용할 때는 원본 객체가 아니라 AOP기능이 적용된 프록시 객체를 사용해야 할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;그런데 @Configuration안에서 @Bean메서드를 직접 호출했을 때 Spring이 이를 관리하지 못하면,&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;AOP가 적용되지 않은 원본 객체를 직접 생성하여 사용하게 될 위험이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;Spring은 CGLIB 프록시를 통해 @Bean 메서드 호출을 가로채고&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;Container가 관리하는 Bean을 반환합니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;그래야 @Transactional이 적용된 Bean, @Cacheable이 적용된 Bean, @Async가 적용된 Bean,&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;scope가 적용된 Bean 같은 Spring의 부가 기능이 유지됩니다.&lt;/p&gt;
&lt;p data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;/p&gt;
&lt;blockquote data-end=&quot;5136&quot; data-start=&quot;5036&quot; data-ke-style=&quot;style3&quot;&gt;AOP semantics &lt;br /&gt;= Spring AOP가 적용된 Bean이라면, 원본 객체가 아니라 AOP 프록시가 적용된 올바른 Bean을 사용하도록 보장하는 것&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-end=&quot;5207&quot; data-start=&quot;5165&quot; data-section-id=&quot;x1vm1s&quot; data-ke-size=&quot;size26&quot;&gt;6. 만약 @Configuration이 아니라 @Component라면?&lt;/h2&gt;
&lt;p data-end=&quot;5257&quot; data-start=&quot;5209&quot; data-ke-size=&quot;size16&quot;&gt;다음처럼 @Component 클래스 안에서도 @Bean을 사용할 수는 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Component
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5576&quot; data-start=&quot;5535&quot; data-ke-size=&quot;size16&quot;&gt;이 코드도 @Bean 메서드 자체는 Spring이 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;5576&quot; data-start=&quot;5535&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5576&quot; data-start=&quot;5535&quot; data-ke-size=&quot;size16&quot;&gt;다만 중요한 차이가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;5850&quot; data-start=&quot;5596&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Configuration 클래스 안의 @Bean 메서드는 Spring이 프록시로 가로채서 Bean을 보장합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;5850&quot; data-start=&quot;5596&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반면 @Component 안의 @Bean 메서드는 이른바 &lt;b&gt;lite mode&lt;/b&gt;로 처리됩니다.&lt;/p&gt;
&lt;p data-end=&quot;5850&quot; data-start=&quot;5596&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 같은 클래스 안에서 다른 @Bean 메서드를 직접 호출하면 Spring이 이를 프록시로 가로채지 않고,&lt;/p&gt;
&lt;p data-end=&quot;5850&quot; data-start=&quot;5596&quot; data-ke-size=&quot;size16&quot;&gt;일반 Java 메서드 호출처럼 동작합니다.&lt;/p&gt;
&lt;p data-end=&quot;5850&quot; data-start=&quot;5596&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5850&quot; data-start=&quot;5596&quot; data-ke-size=&quot;size16&quot;&gt;즉, 다음 호출이 있을 때&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;new MemberService(memberRepository());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6010&quot; data-start=&quot;5920&quot; data-ke-size=&quot;size16&quot;&gt;memberRepository() 호출이 Spring Container에서 기존 Bean을 꺼내오는 방식이 아니라,&lt;/p&gt;
&lt;p data-end=&quot;6010&quot; data-start=&quot;5920&quot; data-ke-size=&quot;size16&quot;&gt;일반 메서드 호출처럼 동작할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;6010&quot; data-start=&quot;5920&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 아래와 같은 상태가 될 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Spring Container
- memberRepository Bean: MemoryMemberRepository A
- memberService Bean: MemberService

MemberService 내부
- MemoryMemberRepository B 사용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2373&quot; data-start=&quot;2333&quot; data-ke-size=&quot;size16&quot;&gt;즉, MemberRepository 객체가 두 개 생길 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;2373&quot; data-start=&quot;2333&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6075&quot; data-start=&quot;6012&quot; data-ke-size=&quot;size16&quot;&gt;그래서 @Bean 메서드끼리 서로 호출하는 구조라면 @Configuration을 사용하는 것이 안전합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-end=&quot;6133&quot; data-start=&quot;6082&quot; data-section-id=&quot;omqad6&quot; data-ke-size=&quot;size26&quot;&gt;7. @Configuration(proxyBeanMethods = false)는 뭘까?&lt;/h2&gt;
&lt;p data-end=&quot;6172&quot; data-start=&quot;6135&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 코드를 보다 보면 이런 형태도 자주 보입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6452&quot; data-start=&quot;6366&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 @Configuration은 proxyBeanMethods = true입니다.&lt;/p&gt;
&lt;p data-end=&quot;6452&quot; data-start=&quot;6366&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;즉, @Bean 메서드 호출을 프록시로 관리합니다.&lt;/p&gt;
&lt;p data-end=&quot;6562&quot; data-start=&quot;6454&quot; data-ke-size=&quot;size16&quot;&gt;그런데 proxyBeanMethods = false로 설정하면 프록시 생성을 하지 않습니다.&lt;br /&gt;이 경우 @Bean 메서드 간 직접 호출을 통한 싱글톤 보장 기능은 사용하지 않게 됩니다.&lt;/p&gt;
&lt;blockquote data-end=&quot;6581&quot; data-start=&quot;6564&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 언제 사용할 수 있을까요?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;6642&quot; data-start=&quot;6583&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6642&quot; data-start=&quot;6583&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Bean 메서드끼리 직접 호출하지 않고, 의존성을 파라미터로 주입받는 방식이라면 사용할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public MemberService memberService(MemberRepository memberRepository) {
        return new MemberService(memberRepository);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7094&quot; data-start=&quot;6981&quot; data-ke-size=&quot;size16&quot;&gt;이 구조에서는 memberService()가 memberRepository()를 직접 호출하지 않습니다.&lt;br /&gt;대신 Spring이 MemberRepository Bean을 파라미터로 주입합니다.&lt;/p&gt;
&lt;p data-end=&quot;7278&quot; data-start=&quot;7096&quot; data-ke-size=&quot;size16&quot;&gt;따라서 프록시 기반의 inter-bean method call 보장이 필요하지 않습니다.&lt;br /&gt;Spring Framework 5.2 이후에는 이런 최적화를 위해 @Configuration(proxyBeanMethods = false)를 사용할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Bean&amp;nbsp;메서드끼리&amp;nbsp;직접&amp;nbsp;호출한다&lt;br /&gt;&amp;rarr;&amp;nbsp;@Configuration&amp;nbsp;사용&lt;br /&gt;&lt;br /&gt;@Bean&amp;nbsp;메서드끼리&amp;nbsp;직접&amp;nbsp;호출하지&amp;nbsp;않고&amp;nbsp;파라미터&amp;nbsp;주입을&amp;nbsp;쓴다&lt;br /&gt;&amp;rarr;&amp;nbsp;@Configuration(proxyBeanMethods&amp;nbsp;=&amp;nbsp;false)도&amp;nbsp;가능&lt;/blockquote&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;div&gt;
&lt;h4 data-end=&quot;7489&quot; data-start=&quot;7464&quot; data-section-id=&quot;1uqzt1z&quot; data-ke-size=&quot;size20&quot;&gt;8. 실무에서는 어떻게 나눠 쓰면 될까?&lt;/h4&gt;
&lt;p data-end=&quot;7537&quot; data-start=&quot;7516&quot; data-section-id=&quot;125e0zq&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;내가 직접 만든 비즈니스 클래스&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;7600&quot; data-start=&quot;7539&quot; data-ke-size=&quot;size16&quot;&gt;서비스, 레포지토리, 컨트롤러처럼 애플리케이션 내부에서 직접 만든 클래스는 보통 컴포넌트 스캔으로 등록합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
public class MemberService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Repository
public class MemberRepository {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RestController
public class MemberController {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7813&quot; data-start=&quot;7777&quot; data-ke-size=&quot;size16&quot;&gt;이 경우에는 @Component 계열 애노테이션을 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;내가 만든 서비스/레포지토리/컨트롤러
&amp;rarr; @Service, @Repository, @Controller, @Component&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7920&quot; data-start=&quot;7898&quot; data-section-id=&quot;1ae3ftf&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;외부 라이브러리 객체나 설정 객체&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;8009&quot; data-start=&quot;7922&quot; data-ke-size=&quot;size16&quot;&gt;반대로 내가 직접 클래스에 애노테이션을 붙일 수 없거나,&lt;/p&gt;
&lt;p data-end=&quot;8009&quot; data-start=&quot;7922&quot; data-ke-size=&quot;size16&quot;&gt;생성 과정에 별도 설정이 필요한 객체는 @Configuration + @Bean으로 등록합니다.&lt;/p&gt;
&lt;p data-end=&quot;8034&quot; data-start=&quot;8011&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 객체들이 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig {

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;8558&quot; data-start=&quot;8503&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 다음 객체들은 @Configuration + @Bean으로 등록하는 경우가 많습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RestTemplate
ObjectMapper
PasswordEncoder
SecurityFilterChain
CorsConfigurationSource
외부 SDK Client
설정값이 필요한 객체&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;8699&quot; data-start=&quot;8685&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;직접 만든 클래스
&amp;rarr; @Component 계열

생성 로직을 직접 제어해야 하는 객체
&amp;rarr; @Configuration + @Bean&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-end=&quot;8803&quot; data-start=&quot;8792&quot; data-section-id=&quot;10sd3ci&quot; data-ke-size=&quot;size20&quot;&gt;9. 최종 정리&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;@Component
= 이 클래스 자체를 Bean으로 등록해줘

@Configuration
= 이 클래스는 Bean 등록 설정 클래스야

@Bean
= 이 메서드가 반환하는 객체를 Bean으로 등록해줘&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;8994&quot; data-start=&quot;8945&quot; data-ke-size=&quot;size16&quot;&gt;그리고 @Configuration과 @Component의 핵심 차이는 다음입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Component는 일반 Bean 등록용이다.

@Configuration은 Bean 설정 클래스이며,
@Bean 메서드 간 호출을 Spring이 프록시로 관리해 싱글톤이 깨지지 않도록 도와준다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;9134&quot; data-start=&quot;9125&quot; data-section-id=&quot;1y0wfhr&quot; data-ke-size=&quot;size26&quot;&gt;  결론&lt;/h2&gt;
&lt;p data-end=&quot;9214&quot; data-start=&quot;9136&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 @Configuration도 내부적으로 @Component를 포함하고 있기 때문에 둘의 차이가 모호하게 느껴질 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;9214&quot; data-start=&quot;9136&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;9234&quot; data-start=&quot;9216&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 차이는 명확했습니다.&lt;/p&gt;
&lt;p data-end=&quot;9373&quot; data-start=&quot;9236&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Component는&lt;/b&gt; 클래스 자체를 Bean으로 등록하기 위한 &lt;b&gt;일반적인 자동 등록 방식&lt;/b&gt;입니다.&lt;br /&gt;&lt;b&gt;@Configuration은&lt;/b&gt; Bean 등록을 위한 설정 클래스이고, &lt;b&gt;내부의 @Bean 메서드가 반환하는 객체들도 Bean으로 등록&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-end=&quot;9414&quot; data-start=&quot;9375&quot; data-ke-size=&quot;size16&quot;&gt;특히 중요한 부분은 @Configuration의 프록시 동작입니다.&lt;/p&gt;
&lt;p data-end=&quot;9414&quot; data-start=&quot;9375&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;9543&quot; data-start=&quot;9416&quot; data-ke-size=&quot;size16&quot;&gt;@Configuration을 사용하면 같은 설정 클래스 안에서 @Bean 메서드끼리 직접 호출하더라도 Spring이 이를 가로채서 이미 등록된 Bean을 반환합니다.&lt;br /&gt;그래서 싱글톤이 깨지는 문제를 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;9563&quot; data-start=&quot;9545&quot; data-ke-size=&quot;size16&quot;&gt;결국 이렇게 정리할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;내가 만든 서비스 클래스라면 @Service 또는 @Component

외부 라이브러리 객체를 Bean으로 등록해야 한다면 @Configuration + @Bean

@Bean 메서드끼리 직접 호출한다면 @Configuration을 사용하는 것이 안전&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;9849&quot; data-start=&quot;9719&quot; data-ke-size=&quot;size16&quot;&gt;Spring의 Bean 등록 방식은 단순히 자동이냐 수동이냐의 문제가 아니었습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;9849&quot; data-start=&quot;9719&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;어떤 객체를 누가 생성하고, 어떤 방식으로 Container가 관리하며,&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;9849&quot; data-start=&quot;9719&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;싱글톤을 어떻게 보장하는지까지 연결해서 이해해야 하는 개념이었습니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/52</guid>
      <comments>https://xeulbn-dev.tistory.com/52#entry52comment</comments>
      <pubDate>Sun, 7 Jun 2026 20:02:09 +0900</pubDate>
    </item>
    <item>
      <title>[CS Study] 네트워크 통신은 왜 비싸다고 하는 것일까? 이게 개발과 무슨 관련일까?</title>
      <link>https://xeulbn-dev.tistory.com/51</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;네트워크 통신은 비싼 작업이다.&lt;/span&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;이에 저는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이게 개발하는 것과 어떻게 관련되어있는지&lt;br /&gt;왜 이 네트워크 통신 비용을 생각해야하는지&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 의문이 들었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이 네트워크 통신이 비싼 작업이라는 말이 굉장히 추상적으로 들렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 API를 호출하는 것도 결국 코드 한 줄이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 데이터를 저장하는 것도 Repository 메서드 하나를 호출하는 것처럼 보이기 때문입니다.&lt;/p&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;반복문 안에서 외부 API를 계속 호출하거나, DB에 데이터를 한 건씩 Insert하거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지와 JSON 응답 크기가 커질 때 순간 응답 시간이 눈에 띄게 느려지는 경우가 발생합니다.&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;위와 같은 현상이 발생할 수 있는 것임을 최근에 깨닫게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 그래서 네트워크 통신이 왜 성능을 저하시키는 작업인지 알아보고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이론을 바탕으로 쇼핑몰 모아보기 서비스의 크롤링 속도를 어떻게 튜닝할 수 있는지 정리해보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 빛도 지구를 무한히 빠르게 돌지는 못합니다. (물리 지식이 다량(?) 함유되어 있기에 Skip하셔도 좋습니다 ㅎㅎ)&lt;/h4&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;진공 상태에서 빛의 속도는 정확히 초속 299,792,458m, 즉 약 299,792km/s 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지구의 적도 지름은 약 12,756km이고(NASA에서 설명하는 지구 적도 지름),&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기준으로 적도 둘레를 계산하면 약 40,075 km 정도가 됩니다.&lt;/p&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초에 지구를 몇 바퀴나 돌 수 있을까요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;빛의 속도 ≒ 299,792km/s &lt;br /&gt;지구 둘레 ≒ 40,075km &lt;br /&gt;&lt;br /&gt;299,792 / 40,075 ≒ 7.48&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;즉 빛은 1초에 지구를 약 7.5바퀴 돌 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빛조차도 지구 한 바퀴를 도는데 약 133ms가 걸리게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;40,075km / 299,792km/s ≒ 0.133초&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;개발자가 보는 100ms, 200ms, 500ms는 코드 레벨에서는 짧아 보일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 네트워크 관점에서는 이미 지구 규모의 거리와 물리적인 전파 시간이 포함된 수치입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 실제 인터넷 통신은 진공 속 빛의 속도로 직선 이동하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광섬유 안에서의 전파 속도는 진공보다 느리고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터 / 스위치 / ISP / 해저 케이블 / 중간 경로를 거치면서 더 많은 지연이 붙습니다.&lt;/p&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;물리적으로 왕복하는 데 필요한 시간이 응답 시간에 포함되어 느려질 수도 있는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. TCP 통신이 어떻게 돌아가더라?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 우리가 서버와 통신할 때 자주 사용하는 TCP 프로토콜도 살펴볼 필요가 있습니다.&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는 데이터를 그냥 던지고 끝내지 않습니다.&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;TCP 연결 수립은 컴퓨터 네트워크 시간에 배우는 것과 같이 three-way handshake 절차로 이루어지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순하게 TCP 연결을 새로 열고 HTTP 요청을 보낸 뒤 연결을 닫는다고 가정하면 아래와 같은 흐름이 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;892&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw7pcA/dJMcab5qT0y/PqfFesN4bbtQkwecigQKC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw7pcA/dJMcab5qT0y/PqfFesN4bbtQkwecigQKC0/img.png&quot; data-alt=&quot;TCP 통신&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw7pcA/dJMcab5qT0y/PqfFesN4bbtQkwecigQKC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw7pcA%2FdJMcab5qT0y%2FPqfFesN4bbtQkwecigQKC0%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;1280&quot; height=&quot;892&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;892&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TCP 통신&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client &amp;rarr; Server : SYN&lt;/li&gt;
&lt;li&gt;Server &amp;rarr; Client : SYN + ACK&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Client &amp;rarr; Server : ACK&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server 와 Client가 연결된 이후&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client &amp;rarr; Server : HTTP Request&lt;/li&gt;
&lt;li&gt;Server &amp;rarr; Client : HTTP Response&lt;/li&gt;
&lt;/ol&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;Client &amp;rarr; Server : FIN&lt;/li&gt;
&lt;li&gt;Server &amp;rarr; Client : ACK&lt;/li&gt;
&lt;li&gt;Server &amp;rarr; Client : FIN&lt;/li&gt;
&lt;li&gt;Client &amp;rarr; Server : ACK&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 단순화 했을 때, 총 9번의 패킷 교환이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 실제로는 HTTP Keep-Alive, HTTP/2, HTTP/3, TLS, 커넥션 풀링 등에 따라 이 흐름은 달라지게 됩니다.)&lt;/p&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;빛이 지구 한바퀴를 도는 데만 약 133ms가 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지구 4.5바퀴에 해당하는 거리는 이론적으로 약 600ms가 걸리는 것에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이야기를 한 이유는,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 번의 왕복이 누적되면 물리적 거리 비용이 커진다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국에서 미국 서버와 통신할 때는 네트워크 경로, 리전, ISP, CDN 여부에 따라 다르지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왕복 지연 시간인 RTT가 수십 ms에서 수백 ms까지 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;1188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mQfwe/dJMcabLcaj1/aspaaum0TyFy5N5l4UJ4Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mQfwe/dJMcabLcaj1/aspaaum0TyFy5N5l4UJ4Dk/img.png&quot; data-alt=&quot;출처 : https://www.cloudflare.com/learning/performance/glossary/what-is-latency&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mQfwe/dJMcabLcaj1/aspaaum0TyFy5N5l4UJ4Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmQfwe%2FdJMcabLcaj1%2Faspaaum0TyFy5N5l4UJ4Dk%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;1584&quot; height=&quot;1188&quot; data-origin-width=&quot;1584&quot; data-origin-height=&quot;1188&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://www.cloudflare.com/learning/performance/glossary/what-is-latency&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloudflare 공식문서에서도 지연 시간은 대표적으로 물리적 거리에 의해서 발생한다는 점을 이야기하고 있는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Latency와 Bandwidth&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Latency는 요청을 보내고 응답이 오기까지 걸리는 지연 시간입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bandwidth는 한 번에 얼마나 많은 데이터를 보낼 수 있는지를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인터넷 속도가 100Mbps라고 가정해보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;100Mbps&amp;nbsp;=&amp;nbsp;100&amp;nbsp;Megabit&amp;nbsp;per&amp;nbsp;second&lt;br /&gt;100&amp;nbsp;/&amp;nbsp;8&amp;nbsp;=&amp;nbsp;12.5MB/s&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;위 식에 따라 이론적으로는 초당 12.5MB정도를 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버가 지구 반대편에 있고 RTT가 높다면 첫 응답을 받기까지는 여전히 시간이 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Latency가 낮아도 이미지, 동영상, 대용량 JSON처럼 전송해야 할 데이터가 크면 Bandwidth 한계에 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 네트워크 성능 문제는 아래 두 가지로 갈리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1.&amp;nbsp;왕복이&amp;nbsp;너무&amp;nbsp;많아서&amp;nbsp;느린&amp;nbsp;경우&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;Latency&amp;nbsp;문제&lt;br /&gt;&lt;br /&gt;2.&amp;nbsp;한&amp;nbsp;번에&amp;nbsp;보내는&amp;nbsp;데이터가&amp;nbsp;너무&amp;nbsp;커서&amp;nbsp;느린&amp;nbsp;경우&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;Bandwidth&amp;nbsp;문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹과 모바일 서비스에서는 아래의 데이터들이 계속 네트워크를 통해 이동하게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지&lt;/li&gt;
&lt;li&gt;동영상&lt;/li&gt;
&lt;li&gt;API Json 응답&lt;/li&gt;
&lt;li&gt;서버와 DB 사이의 쿼리 결과&lt;/li&gt;
&lt;li&gt;마이크로 서비스 간 요청/응답 데이터&lt;/li&gt;
&lt;li&gt;로그, 이벤트, 메시지 큐 payload&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등등...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터들의 크기가 커지면 대역폭을 결국에는 더 많이 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 대역폭보다 많은 데이터를 동시에 보내려고 하면 큐잉, 지연, 타임아웃 및 재전송이 발생할 수 밖에 없는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 그래서 네트워크 통신은 횟수와 크기를 모두 줄여야 합니다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 그냥 토이 프로젝트라면 이런 부분까지 세심하게 신경 쓸 이유는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로, 한국에서 한국 서버로 서빙하거나, 미국에서 미국 서버로 서빙하기도 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주고 받는 데이터의 양 자체가 그렇게 크지 않은 경우가 많기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 대규모의 프로젝트가 되고 글로벌 프로젝트가 된다면 이야기가 달라집니다.&lt;/p&gt;
&lt;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;/li&gt;
&lt;li&gt;통신 데이터 크기를 줄입니다.&lt;/li&gt;
&lt;li&gt;이미 필요한 통신이라면 대역폭을 최대한 활용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 DB에 데이터를 저장할 때 아래의 코드는 성능상 불리할 수 밖에 없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779973895679&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (Product product : products) {
    productRepository.save(product);
}&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;보기에는 굉장히 단순해 보이지만, 내부적으로 DB와의 통신이 여러번 가게 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 몇 십건 단위면 큰 문제가 없을 수 있지만 몇 십만건 단위가 되면 큰 문제가 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이럴 때는 어떻게 하면 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 Batch Insert 또는 Bulk Insert를 고려합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779973966475&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jdbcTemplate.batchUpdate(sql, batchArgs);&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;이렇게 되면 DB에 한 건씩 왕복하여 시간복잡도가 O(n)이 되는 것이 아니라, O(1)로 수렴하도록 만들 수 있습니다.&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;HTTP 응답에서도 마찬가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API에서 반환하는 JSON에 불필요한 필드, 공백, 개행, 중복 데이터가 많다면 그만큼 응답 크기가 커집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 프레임워크나 웹 서버는 JSON 직렬화 과정에서 불필요한 공백을 제거하거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;HTTP의 Content-Encoding을 통해 gzip과 같은 압축 방식을 사용할 수도 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 끽해봐야 JSON? JSON 오브젝트 하나는 필드가 많아질수록 어느 정도까지 커질까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로그 분석 결과 1건을 JSON으로 내려준다고 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780034468289&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;resultId&quot;: 1,
  &quot;logDate&quot;: &quot;2026-05-29T10:15:30&quot;,
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;clientIp&quot;: &quot;123.123.123.123&quot;,
  &quot;requestUrl&quot;: &quot;/api/login&quot;,
  &quot;ruleType&quot;: &quot;BRUTE_FORCE&quot;,
  &quot;riskLevel&quot;: &quot;HIGH&quot;,
  &quot;summary&quot;: &quot;5분 내 로그인 실패 10회 발생&quot;,
  &quot;status&quot;: &quot;DETECTED&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도가 들어가게 될 경우, 아래 사진에서 확인할 수 있는 바와 같이 280바이트가 나오게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;1360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/briBd6/dJMcad3cW2i/EHJTRx7kPwxxZ0cq6vHHz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/briBd6/dJMcad3cW2i/EHJTRx7kPwxxZ0cq6vHHz0/img.png&quot; data-alt=&quot;실제 저장 JSON 크기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/briBd6/dJMcad3cW2i/EHJTRx7kPwxxZ0cq6vHHz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbriBd6%2FdJMcad3cW2i%2FEHJTRx7kPwxxZ0cq6vHHz0%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;794&quot; height=&quot;1360&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;1360&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 저장 JSON 크기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 실제 로그 필드는 여기서 끝나는게 아니라 필드가 더욱 늘어날 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 아래와 같이 늘어났다고 해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1780034619977&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;resultId&quot;: 1,
  &quot;inspectionId&quot;: 10,
  &quot;systemName&quot;: &quot;고객정보관리시스템&quot;,
  &quot;logDate&quot;: &quot;2026-05-29T10:15:30&quot;,
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;userName&quot;: &quot;홍길동&quot;,
  &quot;department&quot;: &quot;개발팀&quot;,
  &quot;clientIp&quot;: &quot;123.123.123.123&quot;,
  &quot;requestUrl&quot;: &quot;/api/admin/users&quot;,
  &quot;httpMethod&quot;: &quot;POST&quot;,
  &quot;userAgent&quot;: &quot;Mozilla/5.0 ...&quot;,
  &quot;ruleType&quot;: &quot;UNAUTHORIZED_IP_LOGIN&quot;,
  &quot;ruleName&quot;: &quot;비인가 IP 로그인 성공 점검&quot;,
  &quot;riskLevel&quot;: &quot;CRITICAL&quot;,
  &quot;reason&quot;: &quot;허용되지 않은 IP에서 로그인 성공&quot;,
  &quot;evidenceCount&quot;: 12,
  &quot;status&quot;: &quot;REQUESTED&quot;,
  &quot;createdAt&quot;: &quot;2026-05-29T11:00:00&quot;,
  &quot;updatedAt&quot;: &quot;2026-05-29T11:10:00&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;JSON 오브젝트 안에서 줄이 몇개 늘어났을 뿐인데, 643바이트까지 늘어난 것을 확인할 수 있습니다.&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;784&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blxNpk/dJMcah5HrbF/kWYIcqVzSNWXoCVnvKH9n1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blxNpk/dJMcah5HrbF/kWYIcqVzSNWXoCVnvKH9n1/img.png&quot; data-alt=&quot;실제 저장 JSON 크기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blxNpk/dJMcah5HrbF/kWYIcqVzSNWXoCVnvKH9n1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblxNpk%2FdJMcah5HrbF%2FkWYIcqVzSNWXoCVnvKH9n1%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;784&quot; height=&quot;478&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 저장 JSON 크기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 여기에 requestHeaders, ResponseBody 같은 값을 같이 내려주게 되면 1건이 1키로바이트가 넘어갈 수도 있는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 그렇다면 한 번에 주고받는 JSON 크기는 어느 정도가 최대일까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수하게 궁금해졌습니다. 한 건당 1킬로바이트가 넘어가기 쉬운데, 1번에 주고받는 JSON 크기는 과연 어느 정도까지가 최대일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보고 공부해본 바로는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정해진 최대값은 없고, 설정에 따라 달라진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 답이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 결국 HTTP body에 실려 가는 텍스트일 뿐이라, 크기 한계는 JSON 자체가 아니라&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;대표적으로 Nginx는 요청 body 최대 크기를 client_max_body_size로 제한합니다. &lt;br /&gt;기본값은 1MB이고, 요청 크기가 이 값을 넘으면 413 Request Entity Too Large 에러가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 한계는 여기 하나만 있는 게 아닙니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;WAS 계층&lt;/b&gt;: Spring Boot라면 spring.servlet.multipart.max-request-size, &lt;br /&gt;Tomcat이라면 maxPostSize 등이 따로 걸려 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;애플리케이션 계층&lt;/b&gt;: JSON 파서(Jackson 등)가 역직렬화할 때 메모리에 통째로 올리므로, &lt;b&gt;큰 payload는 곧 힙 사용량입니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트/프록시 계층&lt;/b&gt;: CDN, API Gateway, 로드밸런서마다 각자의 body 크기 제한이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;JSON을 얼마나 크게 보낼 수 있나&quot;는 &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;물론 client_max_body_size를 10MB, 50MB로 늘려도 됩니다.&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;payload가 커질수록 직렬화/역직렬화 CPU 비용, 서버 메모리 점유, 그리고 무엇보다 3번에서 본 대역폭 한계에 그대로 부딪힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;보낼 수 있다&quot;가 &quot;보내야 한다&quot;를 의미하진 않는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 큰 데이터를 다뤄야 한다면 설정값을 키우기보다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;페이지네이션 / 커서 기반 조회&lt;/b&gt;로 한 번에 내려주는 양 자체를 쪼개거나,&lt;/li&gt;
&lt;li&gt;파일이나 이미지처럼 큰 바이너리는 body에 싣지 않고 &lt;b&gt;Presigned URL로 스토리지에서 직접&lt;/b&gt; 받게 하거나,&lt;/li&gt;
&lt;li&gt;꼭 필요한 필드만 내려주는 &lt;b&gt;부분 응답(partial response) / GraphQL 스타일 필드 선택&lt;/b&gt;을 고려하는 편이 낫습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 저는 &quot;네트워크 통신은 비싼 작업이다&quot;라는 말이 추상적이라고 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 한 번의 왔다갔다인데 뭐가 비싸다는 거지, 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정리하고 보니 그 한 줄 뒤에는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;빛조차 지구 한 바퀴에 133ms가 걸리는 물리적 거리&lt;/b&gt;, &lt;b&gt;연결 하나를 맺고 끊는 데만 여러 번 오가는 패킷 등 &lt;/b&gt;수많은 작업들이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 코드 레벨에서 보는 &quot;겨우 200ms&quot;는, 네트워크 입장에서는 이미 지구 규모의 비용이 응축된 숫자였던 것입니다.&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;b&gt;통신 횟수를 줄인다&lt;/b&gt;&amp;nbsp;&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 토이 프로젝트에서는 여기까지 신경 쓸 이유가 없을 수도 있습니다. 오히려 과잉 설계에 과잉 걱정이 될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터가 수십만 건이 되고, 서버가 지구 반대편에 있고, 트래픽이 글로벌해지는 순간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작은 비용들이 누적되어 서비스의 체감 속도를 좌우하게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/51</guid>
      <comments>https://xeulbn-dev.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 29 May 2026 15:25:37 +0900</pubDate>
    </item>
    <item>
      <title>[CS Study] 디자인 패턴의 근본 : 객체지향</title>
      <link>https://xeulbn-dev.tistory.com/50</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  들어가기 전에&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어를 오래 들여다보면, 결국 코드가 동작하는지보다 바뀔 수 있는지가 더 중요한 순간이 오게 됩니다. 처음 짤 때는 모두 동작은 합니다. 하지만 6개월 뒤, 요구사항이 바뀌고 새 기능이 끼어들 때 비로소 그 코드가 &quot;잘 설계되었는가&quot;의 질문이 나오게 됩니다.&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;b&gt;실행 중에 제대로 동작할 것&lt;/b&gt; &amp;mdash; 모듈의 존재 이유 그 자체다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경을 위해 존재할 것&lt;/b&gt; &amp;mdash; 대부분의 모듈은 생명주기 동안 변경되므로, 간단한 작업만으로 변경 가능해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드를 읽는 사람과 소통할 것&lt;/b&gt; &amp;mdash; 특별한 훈련 없이도 개발자가 쉽게 읽고 이해할 수 있어야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 두 번째와 세 번째 목적, 즉 &lt;b&gt;&quot;변경에 강한 코드를 어떻게 만들 것인가&quot;&lt;/b&gt; 에 대한 정리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 설계라는 도구를 통해 의존성, 결합도, 응집도, 캡슐화, 책임의 이동이라는 다섯 개념이 서로 어떻게 맞물리는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그것이 왜 아키텍처의 출발점이 되는지를 Java 예시 코드와 함께 짚어보려 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  본론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로그래밍 패러다임과 모듈이 존재하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어가 선택한 패러다임은 우리가 코드를 바라보는 방식을 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C는 절차형 패러다임 위에 서 있고, Java는 객체형 패러다임 위에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 패러다임의 대표 주자는 LISP이며, 논리형은 PROLOG가 대표적입니다.&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;예를 들어 &quot;영화관에 손님이 입장한다&quot;는 단순한 시나리오를 절차적으로 풀면 다음과 같이 보일 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1778898294091&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 절차지향 스타일 &amp;mdash; Theater가 모든 데이터를 직접 조작합니다
public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Theater는 Audience의 가방을 열어보고, TicketSeller의 매표소에 손을 집어넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 객체도 자기 일을 스스로 처리하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 가진 객체와, 그 데이터를 처리하는 코드가 분리되어 있기 때문에 한쪽이 바뀌면 다른 한쪽도 같이 바뀌게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 모듈의 두 번째 목적 &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;2. 의존성과 결합도&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성은 단순히 &quot;A가 B를 사용한다&quot;라는 사실에 머무르지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성이라는 단어 속에는 &lt;b&gt;B가 변경되면 A도 함께 변경될 수 있다&lt;/b&gt;는 변경의 그림자가 같이 들어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 사이의 의존이 과한 상태를 가리켜 우리는 &lt;b&gt;결합도가 높다(high coupling)&lt;/b&gt; 고 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 합리적인 수준으로 의존할 때 결합도가 낮다고 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결합도가 높을수록 두 객체가 함께 변경될 확률이 커지고, 자연스레 변경이 어려워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 Theater.enter()를 다시 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Audience가 가방을 들고 다니지 않고 지갑을 든다고 요구사항이 바뀌면 어떻게 될까요?&lt;/span&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;audience.getBag() 호출이 깨지고, Theater의 enter 메서드를 함께 고쳐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TicketSeller가 매표소가 아니라 디지털 발권 시스템을 쓴다고 해도 마찬가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터의 변경이 지역적으로 고립되지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결의 단서는 의존성을 없애는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;가장 기본적인 도구는 인터페이스와 구현을 분리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 내부의 상태를 캡슐화하고, 객체 사이에는 오직 메시지로만 상호작용하도록 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Theater는 TicketSeller의 내부를 몰라도 됩니다. 단지 sellTo라는 메시지를 이해하고 응답할 수 있다는 사실만 알면 충분합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1778898457883&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 캡슐화된 TicketSeller &amp;mdash; 매표소(내부 상태)는 숨기고 행동만 노출합니다.
public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }

    // 퍼블릭 인터페이스: &quot;이 관객에게 표를 팔아 줘&quot;
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TicketOffice는 더 이상 외부에 노출되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것이 private라는 사실 자체가 &quot;내가 가진 매표소는 나만 만진다&quot;는 약속인 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 매표소 구조를 바꾸더라도 TicketSeller의 퍼블릭 인터페이스가 유지되는 한, 다른 객체는 영향을 받지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 &lt;b&gt;인터페이스와 구현의 분리&lt;/b&gt; 원칙이 결합도를 낮추는 방식인 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 캡슐화, 응집도, 그리고 책임의 이동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밀접하게 연관된 작업만을 수행하고, 연관성 없는 작업은 다른 객체에게 위임하는 객체를 우리는 &lt;b&gt;응집도가 높다&lt;/b&gt;고 표현합니다. 데이터를 가진 객체가 스스로 그 데이터를 처리할 수 있도록 만들면, 결합도는 자연스레 낮아지고 응집도는 동시에 올라갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 절차적 코드와 비교해 보면 차이가 분명해집니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1778898496421&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 자율적인 객체로 다시 쓴 Audience
public class Audience {
    private Bag bag;

    public Audience(Bag bag) {
        this.bag = bag;
    }

    // 가방을 열어 표를 사는 일은 Audience 자신이 처리한다
    public Long buy(Ticket ticket) {
        return bag.hold(ticket);
    }
}

// 자율적인 객체로 다시 쓴 Bag
public class Bag {
    private Long amount;
    private Invitation invitation;
    private Ticket ticket;

    public Long hold(Ticket ticket) {
        if (hasInvitation()) {
            setTicket(ticket);
            return 0L;
        } else {
            setTicket(ticket);
            minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }

    private boolean hasInvitation() { return invitation != null; }
    private void setTicket(Ticket ticket) { this.ticket = ticket; }
    private void minusAmount(Long amount) { this.amount -= amount; }
}

// 그 결과 Theater는 한 줄로 줄어든다
public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;절차지향에서는 Theater라는 독재자가 모든 데이터를 들고 와서 처리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향에서는 그 독재자가 사라지고, 각 객체에 책임이 적절하게 분배됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Audience는 자기 가방을, Bag은 자기 안의 표와 금액을, TicketSeller는 자기 매표소를 스스로 책임지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책임의 이동이야말로 절차지향과 객체지향을 가르는 근본적인 차이입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 변경하기 쉬운 설계는 결국 &lt;b&gt;한 번에 하나의 클래스만 변경하면 되는 설계&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가방의 구조가 바뀌면 Bag만 고치면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매표소의 구조가 바뀌면 TicketOffice만 고치면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경의 영향이 지역적으로 고립됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 캡슐화와 접근 제어가 가지는 의미가 분명해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡슐화는 객체를 두 부분으로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 접근 가능한 &lt;b&gt;퍼블릭 인터페이스&lt;/b&gt;, 그리고 외부에서는 접근할 수 없고 오직 내부에서만 접근 가능한 &lt;b&gt;구현&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 상태는 숨기고 행동만 외부에 공개해야 한다는 원칙은 이 두 부분의 경계선을 분명하게 긋는 일인 것 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;설계를 어렵게 만드는 진짜 원인은 의존성입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결의 방향은 불필요한 의존성을 제거함으로써 결합도를 낮추는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 세부사항을 객체 내부로 캡슐화하면, 객체의 자율성이 높아지고 응집도 높은 객체들의 공동체가 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낮은 결합도와 높은 응집도를 가진 자율적인 객체들이, 최소한의 의존성만으로 협력하는 모습,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것이 훌륭한 객체지향이라고 부를 수 있는 상태인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편 잊지 말아야 할 사실이 하나 있습니다. &lt;b&gt;어떤 기능을 설계하는 방법은 한 가지 이상입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 기능을 여러 방법으로 설계할 수 있기에, 설계는 결국 트레이드오프의 산물이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사람을 만족시키는 단 하나의 설계란 존재하지 않으며, 좋은 설계는 균형의 예술에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결합도를 낮추기 위해 인터페이스를 너무 많이 도입하면 코드의 이해 비용이 올라가고, 응집도를 극단까지 끌어올리면 클래스 수가 폭발합니다. 어느 쪽이 옳다고 말하기보다, &quot;이 맥락에서 어느 쪽이 더 합리적인가&quot;를 묻는 감각이 결국 아키텍처를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;객체지향 설계의 핵심은 적절한 객체에 적절한 책임을 할당하는 것이다.&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;&lt;b&gt;그것이 곧 변경에 강한 아키텍처를 향한 첫 걸음이라고 믿습니다.&lt;/b&gt;&lt;/p&gt;</description>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/50</guid>
      <comments>https://xeulbn-dev.tistory.com/50#entry50comment</comments>
      <pubDate>Sat, 16 May 2026 15:10:32 +0900</pubDate>
    </item>
    <item>
      <title>[CS Study] Claude Code &amp;amp; Codex with Harness Engineering</title>
      <link>https://xeulbn-dev.tistory.com/49</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 AI 코딩 에이전트가 주목을 받고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT나 Claude 같은 대형 LLM은 사람 대신 코드를 쓰거나 문서를 정리하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무런 Harness없이 모델만 덩그러니 놓으면 제대로 일을 못하는 모습을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Harness는 모델이 주어진 작업을 안전하고 효율적으로 수행하도록 돕는 도구, 규칙, 실행 환경의 집합입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 프로젝트에서 에이전트의 행동을 설명하는 AGENTS.md, 개별 기능을 정의하는 SKILL.md와 같은 파일을 함께 관맇바니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 좋은 에이전트와 하네스 설계란 어떤 것인지를 알아보려고 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본문&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 하네스 엔지니어링이란?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Harness Engineeering의 사전적 정의는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Harness : 마구 (말을 타거나 부리는데 쓰는 도구)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Engineering : 공학기술&lt;/p&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;lsquo;마구(馬具)&amp;rsquo;처럼 에이전트의 행동을 묶고, 방향을 잡고, 안전하게 제어하는 구조입니다. 구체적으로는 이런 것들을 포함합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이전트에게 주어지는 도구(tool) 목록과 권한 범위&lt;/li&gt;
&lt;li&gt;에이전트에 들어가는 Context 범위 관리&lt;/li&gt;
&lt;li&gt;에이전트가 호출할 수 있는 API/함수의 실행 환경&lt;/li&gt;
&lt;li&gt;로깅 및 모니터링 레이어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하네스 엔지니어링&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;은 AI 에이전트가 장기간에 걸쳐 효과적으로 작업할 수 있도록 이 하네스를 설계하는 일이며, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그 범위는 에이전트 시스템에서 LLM 모델 자체를 제외한 거의 모든 것에 걸쳐 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;프롬프트 구성, 도구 정의, 실행 환경, 세션 관리, 인증, 네트워크 격리, 모니터링까지, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하나하나 보면 이미 우리가 해왔거나 익숙한 개념들이기도 합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;같은 모델이라도 이러한 하네스를 어떻게 설계하느냐에 따라 에이전트의 성능과 안정성이 크게 달라집니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 좋은 에이전트란 어떤 에이전트일까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 코딩을 하면서 느끼는 좋은 에이전트는 물어볼때 물어볼 줄 아는 에이전트인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 같이 일하고 싶은 좋은 사람도 위와 같은 사람이죠..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 API 코드를 모두 작성해두고 &amp;ldquo;다 끝났습니다!&amp;rdquo;라고 공유했지만,&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;특히 외부 API 호출 로직을 구현할 때 이런 문제가 자주 드러납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 로직, 외부 통신, 예외 처리, 데이터 가공까지 하나의 서비스에 강하게 결합해놓고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 처리하도록 작성해두는 경우가 대표적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 동작하는 코드를 보고 빠르고 잘 짰다고 생각이 들 수도 있으나 시간이 지나 요구사항이 추가되거나 장애 상황이 발생하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 범위가 커지고 테스트도 어려워지면서 유지보수 비용이 급격히 증가하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 질문해야 할 때 알아서 잘 질문할 줄 알고 알아서 해야할 때, 알아서 할 줄 아는 에이전트가 좋은 에이전트인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;질문 해야할때는 언제일까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 되돌리기 어려운 작업 (삭제, 배포, 외부 API 호출)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ex) 운영 DB 데이터 삭제 쿼리 실행, Prod 서버 배포 등&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;ex) 동기 처리로 코드를 작성할지, 비동기 이벤트기반으로 분리할지를 결정하는 경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 비동기처리를 하면 안되는 곳에서 갑자기 비동기 처리로 코드를 작성해 놓는다면 어떨까요..?&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 그리고 그 코드가 갑자기 Prod레벨까지 올라가버린다면 어떨까요?&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;ex) EC2 인스턴스 스펙을 대폭 올리거나 Kafka 도입 등 유로 SaaS 도입을 결정하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;알아서 해야할 때는 언제일까요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 안전하게 반복 가능한 작업&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ex) 반복되는 예외 처리&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;ex) 팀에서 정한 API 응답 포맷에 맞춘 DTO 작성&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;ex) Swagger 설명 문구 수정이나 변수명 리팩토링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 위와 같은 불상사를 막고, 알아서 잘 작업을 할 수 있도록 만들어주는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것이 하네스 엔지니어링의 기초이지 않을까 싶습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3. 그럼 어떻게 작성한 SKILL이 좋은 SKILL일까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트는 Human-in-the-Loop(HITL)가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserAskQuestion 같은 도구를 통해 에이전트는 실행 중간에 사람에게 판단을 위임할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 도구를 잘 사용해야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한 적절히 질문하는 때에 질문할 수 있어야하며, 알아서 할 때 알아서 할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 에이전트는 작업 도중 아래와 같은 상황에서 UserAskQuestion을 통해 의도를 명확히 할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;390&quot; data-start=&quot;243&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;277&quot; data-start=&quot;243&quot; data-section-id=&quot;1qlmgmr&quot;&gt;이 작업이 정말 운영 환경(Prod)에 반영되어도 되는가?&lt;/li&gt;
&lt;li data-end=&quot;309&quot; data-start=&quot;278&quot; data-section-id=&quot;wd5seo&quot;&gt;비동기 처리로 변경해도 데이터 정합성 문제가 없는가?&lt;/li&gt;
&lt;li data-end=&quot;344&quot; data-start=&quot;310&quot; data-section-id=&quot;euhi0y&quot;&gt;외부 API 장애 시 재시도 정책은 어떻게 가져갈 것인가?&lt;/li&gt;
&lt;li data-end=&quot;390&quot; data-start=&quot;345&quot; data-section-id=&quot;13vmcsn&quot;&gt;삭제 작업을 Soft Delete로 처리할지 Hard Delete로 처리할지?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 아래와 같은 작업은 컨벤션 기반으로 스스로 처리하는 것이 생산성에 더 도움이 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;744&quot; data-start=&quot;678&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;686&quot; data-start=&quot;678&quot; data-section-id=&quot;1vab75n&quot;&gt;DTO 생성&lt;/li&gt;
&lt;li data-end=&quot;702&quot; data-start=&quot;687&quot; data-section-id=&quot;bz2jlq&quot;&gt;Swagger 문서 작성&lt;/li&gt;
&lt;li data-end=&quot;715&quot; data-start=&quot;703&quot; data-section-id=&quot;34upug&quot;&gt;반복적인 예외 처리&lt;/li&gt;
&lt;li data-end=&quot;728&quot; data-start=&quot;716&quot; data-section-id=&quot;18bjjgp&quot;&gt;네이밍 컨벤션 적용&lt;/li&gt;
&lt;li data-end=&quot;744&quot; data-start=&quot;729&quot; data-section-id=&quot;16xi55f&quot;&gt;테스트 코드 템플릿 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이 경계를 잘 설계해서 SKILL에 녹여내는 것이 굉장히 중요하다고 생각이 듭니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-end=&quot;419&quot; data-start=&quot;384&quot; data-section-id=&quot;wpvfjy&quot; data-ke-size=&quot;size20&quot;&gt;4. Harness 설계 원칙 : 가이드와 센서&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vHFd7/dJMcaffxoYQ/TfOn9davcbqT1MJV3MYyxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vHFd7/dJMcaffxoYQ/TfOn9davcbqT1MJV3MYyxK/img.png&quot; data-alt=&quot;출처 : https://martinfowler.com/articles/harness-engineering.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vHFd7/dJMcaffxoYQ/TfOn9davcbqT1MJV3MYyxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvHFd7%2FdJMcaffxoYQ%2FTfOn9davcbqT1MJV3MYyxK%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;736&quot; height=&quot;258&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://martinfowler.com/articles/harness-engineering.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마틴 파울러는 하네스를 설계할 때 가이드와 센서라는 두 가지 메커니즘을 제안합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-11 오후 9.35.45.png&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;437&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u1475/dJMcafNnbHj/EO5SukulxuFdOBkNKQrXgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u1475/dJMcafNnbHj/EO5SukulxuFdOBkNKQrXgK/img.png&quot; data-alt=&quot;출처 : https://martinfowler.com/articles/harness-engineering.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u1475/dJMcafNnbHj/EO5SukulxuFdOBkNKQrXgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu1475%2FdJMcafNnbHj%2FEO5SukulxuFdOBkNKQrXgK%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;785&quot; height=&quot;437&quot; data-filename=&quot;스크린샷 2026-05-11 오후 9.35.45.png&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;437&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://martinfowler.com/articles/harness-engineering.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가이드 : 에이전트가 코드를 실행하기 전에 행동을 예측하고 방향을 제어하는 요소&lt;/li&gt;
&lt;li&gt;센서 : 실행 후 결과를 감지하고 자기교정하는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 스킬을 실행하기 전에는 코딩 스타일 가이드나 설계 원칙을 프롬프트에 포함해 올바른 방향을 유도하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 후에는 로깅, 테스트, 정적 분석 도구 등을 이용해 오류를 감지하는 것이 센서의 역할입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 파울러는 가이드와 센서가 컴퓨팅적과 추론적이라는 두 차원으로 나뉜다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨팅적 가이드는 예를 들어 함수 정의가 준수되는지 즉시 검증하는 것처럼 명확한 규칙을 자동으로 검사하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추론적 가이드는 설계 원칙이나 비즈니스 규칙처럼 모호한 지침을 인간이나 에이전트가 해석해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(위 2,3번에서 설명한 바와 유사한 맥락입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;센서 또한 자동으로 테스트를 돌리는 컴퓨팅적 센서와 사람이 로그를 보고 의미를 판단하는 추론적 센서가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트의 신뢰성을 높이기 위해서는 두 종류의 가이드와 센서를 적절히 조합하고 하네스의 결과를 지속적으로 검토하여 개선해야 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 토큰 관리와 하네스의 연결고리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하네스를 잘 설계하는 목적 중 하나는 결국 토큰을 낭비하지 않고 모델의 컨텍스트를 효율적으로 사용하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨텍스트가 길어지면 모델이 이전 대화를 잊거나 응답이 느려질 수 있고 API 요금도 증가합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-11 오후 9.45.31.png&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8wWUA/dJMcaaSOhiN/rBsazgkl3WqSLPhngZXuP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8wWUA/dJMcaaSOhiN/rBsazgkl3WqSLPhngZXuP1/img.png&quot; data-alt=&quot;출처 : https://developers.openai.com/api/docs/guides/compaction&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8wWUA/dJMcaaSOhiN/rBsazgkl3WqSLPhngZXuP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8wWUA%2FdJMcaaSOhiN%2FrBsazgkl3WqSLPhngZXuP1%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;778&quot; height=&quot;279&quot; data-filename=&quot;스크린샷 2026-05-11 오후 9.45.31.png&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://developers.openai.com/api/docs/guides/compaction&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Codex와 Anthropic Claude는 이런 문제를 해결하기 위해 토큰 카운팅 API와 Compaction 기능을 제공합니다.&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;Codex에서는 input_tokens.count API로 요청의 토큰 수를 미리 계산하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context_management.compact_threshold를 설정해 긴 대화를 서버에서 자동으로 압축할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-11 오후 9.47.09.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OtyRn/dJMcabYtW5e/PX7icp6swHfv0JOxPmTWzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OtyRn/dJMcabYtW5e/PX7icp6swHfv0JOxPmTWzK/img.png&quot; data-alt=&quot;출처 : https://platform.claude.com/docs/en/build-with-claude/context-windows&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OtyRn/dJMcabYtW5e/PX7icp6swHfv0JOxPmTWzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOtyRn%2FdJMcabYtW5e%2FPX7icp6swHfv0JOxPmTWzK%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;860&quot; height=&quot;764&quot; data-filename=&quot;스크린샷 2026-05-11 오후 9.47.09.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://platform.claude.com/docs/en/build-with-claude/context-windows&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 messages.count_tokens API와 함께 compact_20260112 전략을 도입해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화가 길어지면 오래된 메시지를 요약한 블록으로 대체하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 스스로 생각하는 블록을 다음 턴의 컨텍스트에서 제거하는 Extended Thinking 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 기능을 사용할 때도 AGENTS.md나 SKILL.md에 명시된 규칙을 통해 언제 요약이 일어나야 하는지, 무엇을 유지해야 하는지 정의해 두면 모델이 중요한 정보를 놓치지 않도록 만들 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론 (feat. 조그마한 소신...)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(조그마한 소신...) 그럼 AI가 도래하면서 지금까지 배운 것들이 무의미할까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹자는 이렇게 말하고는 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 도래하면 개발자의 시대가 끝난다고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곧 개발자라는 직업은 사라질것이라고.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;과연 그럴까요?&lt;br /&gt;진짜로 개발자라는 직업은 없어질까요?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말씀드리면, 저의 생각으로는 사라지지는 않을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 어디선가 들었던 말이 있었는데 그 비유가 너무 좋아 가져와보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동차가 등장하기 전에는 마부라는 직업이 존재했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동차의 등장은 결국 마부라는 직업 자체를 사라지게 만들었지만, '사람을 이동시키는 역할'자체가 사라진 것은 아니었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 택시기사, 정비사, 물류 산업과 같은 새로운 형태의 직업과 산업이 등장하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 시대의 개발자 또한 비슷하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 역할과 개발 방식은 분명히 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제를 해결하는 사람이라는 본질 자체는 쉽게 사라지지 않을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼, 지금까지 배운 것들은 앞으로 어떻게 문제를 해결하는 데에 쓰일 수 있을까요?&lt;/span&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;우선, AI가 짜주는 코드를 깊이있게 이해할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해하지 못한 채 AI가 생성한 코드를 그대로 사용하게 되면 결국 유지보수 비용은 시간이 지날수록 커질 수 밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음 MVP를 빠르게 만드는 단계에서는 매우 생산적으로 느껴질 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;하지만 이후 장애 대응, 요구사항 변경, 성능 개선이 필요해지는 순간부터는 이야기가 달라집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 개발자는 AI가 만든 코드를 검증하고, &lt;/span&gt;&lt;span&gt;구조적으로 개선하며, &lt;/span&gt;&lt;span&gt;장기적으로 유지 가능한 형태로 다듬을 수 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이제는 AI를 사용하는 방식 자체도 설계의 영역으로 들어오고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결국 우리가 사용하는 AI의 Context Window와 Token 또한 비용입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무작정 다 쓸 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: start;&quot;&gt;200K 토큰이 많아 보여도, 실제 대규모 코드베이스를 다루다 보면 순식간에 차오릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: start;&quot;&gt;정확한 토큰 수를 하나하나 계산해서 쓸 필요는 없어도 대략적인 토큰 수는 알고 사용해야합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: start;&quot;&gt;그래야 같은 토큰으로도 충분히 효율적인 작업량을 뽑아낼 수 있습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: start;&quot;&gt;예를 들어, Claude는 시작 시 모든 Skill의 메타데이터(name/description)를 시스템 프롬프트에 로드합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333d4b; text-align: start;&quot;&gt;Skill이 20개면 20개의 Description이 항상 Context를 점유합니다.&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;background-color: #ffffff; color: #333d4b; text-align: start;&quot;&gt;SKILL 구조가 아래와 같으면 어떤 문제가 발생할까요?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778123887147&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.claude/skills/
├── review-naming/
│   └── SKILL.md
├── review-types/
│   └── SKILL.md
├── review-complexity/
│   └── SKILL.md
├── review-security/
│   └── SKILL.md
└── ... (15개 더)&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;SKILL이 많아질수록 Description 또한 계속해서 Context를 점유하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 마치 클래스가 폭발하는 안티패턴과 유사한 문제가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 우리가 배웠던 객체지향을 생각해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체는 직접 관련된 객체만 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 내부 구현 세부사항까지 모두 알고 있을 필요는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념을 Skill 설계에 적용해보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SKILL.md 는 결국 진입점만 제공해야하며, 세부 지식은 'refrences/' 와 같은 외부 문서로 위임하는 구조가 더 적절합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 현재 진행 중인 프로젝트의 SKILL 설계입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB2HCn/dJMb997mpPe/qS4kxr3CBMNbT0YM0H3Qe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB2HCn/dJMb997mpPe/qS4kxr3CBMNbT0YM0H3Qe0/img.png&quot; data-alt=&quot;Detoxmate 어플리케이션 Skill 설계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB2HCn/dJMb997mpPe/qS4kxr3CBMNbT0YM0H3Qe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB2HCn%2FdJMb997mpPe%2FqS4kxr3CBMNbT0YM0H3Qe0%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;666&quot; height=&quot;400&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Detoxmate 어플리케이션 Skill 설계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 관리하게 되면 스킬 수가 불필요하게 폭증하는 일도 막을 수 있을 뿐더러,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;refrences와 assets를 통해 학습 자료와 사례를 함께 관리하는 것이 가능합니다.&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;b&gt;무엇보다도!! 가장 중요한 본질은 여전히 크게 달라지지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 아무리 코드를 자동으로 생성한다고 해도, 결국 실제로 실행되는 것은 OS 위에서 동작하는 프로세스와 스레드입니다.&lt;/p&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&gt;Spring Boot 애플리케이션이든, &lt;/span&gt;&lt;span&gt;Python 서버든, &lt;/span&gt;&lt;span&gt;Node.js 애플리케이션이든 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 메모리에 적재되고,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;CPU 스케줄링을 받으며,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;네트워크 소켓과 파일 디스크립터를 사용하는 하나의 프로세스로 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 Latency가 왜 발생하는지, 왜 Thread Pool이 고갈되는지, 왜 Connection Pool에서 병목이 생기는지,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;왜 메모리 사용량이 증가하는지는 결국 개발자가 이해하고 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어, AI가 비동기 처리 코드를 생성했다고 하더라도 그 코드가 실제로는 어떤 Thread에서 실행되는지,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Blocking I/O가 발생하는지, Event Loop를 막고 있지는 않은지, DB Connection을 얼마나 오래 점유하는지는 여전히 중요한 문제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;span&gt;또한, 핵심 키워드들을 알아야 AI는 좀 더 효율적으로 동작합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 Test에 대한 설계 원칙을 모르고 코드를 보고도 어떤 것이 좋은 설계인지를 모른다면, AI에게&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;좋은 Test가 될 수 있도록 설계해줘&quot;&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 명령하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 '좋은' 이라는 워딩 자체가 굉장히 모호합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에 대해 공부한 사람이라면, 아래와 같이 프롬프트를 작성합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;[테스트 코드 작성 규칙] &lt;br /&gt;1. 모든 테스트는 Classicst스타일로 짜야해. &lt;br /&gt;2. 테스트한테 내부 구현을 보여주지 마. &lt;br /&gt;3. 바깥(DB, API)만 모킹하고 안쪽은 진짜를 써. &lt;br /&gt;4. 비즈니스 로직은 서비스가 아니라 데이터 자체에 심어. &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;결국 AI시대에도 운영체제, 네트워크, 데이터베이스, 메모리 구조와 같은 컴퓨터공학의 기초 지식은 여전히 중요하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 적절한 추상화, 낮은 결합도, 높은 응집도, 유지보수 가능한 구조, 변경에 유연한 설계와 같은 것들 또한 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오히려 AI가 더 많은 코드를 빠르게 생성해주는 시대일수록,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그 코드가 시스템 위에서 실제로 어떻게 실행되는지를 이해할 수 있는 개발자의 중요성은 더 커질 수도 있다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고 문서 : &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://toss.tech/article/software-3-0-era&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.tech/article/software-3-0-era&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778105875885&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;소프트웨어 3.0 시대를 맞이하며&quot; data-og-description=&quot;레이어드 아키텍처에 익숙한 개발자가 Claude Code를 바라보는 방법&quot; data-og-host=&quot;toss.tech&quot; data-og-source-url=&quot;https://toss.tech/article/software-3-0-era&quot; data-og-url=&quot;https://toss.tech/article/software-3-0-era&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/9MCtS/dJMb8YpZqlG/1bqKVQQQ4yZuvsDyGIfYu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cVIVym/dJMb8VNzwAX/76S09VbHlVpzKdnMdTnlO1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b9aFzc/dJMb8UHTcwX/LkA3tL40YnMboYBGNIE1h1/img.png?width=1024&amp;amp;height=565&amp;amp;face=0_0_1024_565&quot;&gt;&lt;a href=&quot;https://toss.tech/article/software-3-0-era&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://toss.tech/article/software-3-0-era&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/9MCtS/dJMb8YpZqlG/1bqKVQQQ4yZuvsDyGIfYu1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cVIVym/dJMb8VNzwAX/76S09VbHlVpzKdnMdTnlO1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/b9aFzc/dJMb8UHTcwX/LkA3tL40YnMboYBGNIE1h1/img.png?width=1024&amp;amp;height=565&amp;amp;face=0_0_1024_565');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;소프트웨어 3.0 시대를 맞이하며&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처에 익숙한 개발자가 Claude Code를 바라보는 방법&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;toss.tech&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;(참고 문서 : &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/tech/harness-engineering-from-deep-insight/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://aws.amazon.com/ko/blogs/tech/harness-engineering-from-deep-insight/&lt;/a&gt; )&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778105739180&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;하네스 엔지니어링으로 본 Deep Insight &amp;ndash; 로컬 개발에서 프로덕션 운영까지의 설계 여정 | Amazon Web&quot; data-og-description=&quot;AI에게 단순히 &amp;ldquo;잘 해봐&amp;rdquo;라고 시키는 것과, AI가 스스로 만들고 평가하고 개선하는 Agentic 시스템을 설계하는 건 완전히 다른 결과물을 만들어냅니다. AWS Korea SA Team은 Agentic AI 시스템을 개발할 &quot; data-og-host=&quot;aws.amazon.com&quot; data-og-source-url=&quot;https://aws.amazon.com/ko/blogs/tech/harness-engineering-from-deep-insight/&quot; data-og-url=&quot;https://aws.amazon.com/ko/blogs/tech/harness-engineering-from-deep-insight/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBT442/dJMb88e4Il6/ypZMkV2tAIAFer8QyBJoOK/img.png?width=956&amp;amp;height=630&amp;amp;face=0_0_956_630,https://scrap.kakaocdn.net/dn/yD4bf/dJMb82eQ2CM/6Op4Y0R7gIXqfBK3hH3fnK/img.png?width=956&amp;amp;height=630&amp;amp;face=0_0_956_630,https://scrap.kakaocdn.net/dn/bP5UDa/dJMb88e4Il8/bozKhVkJX7Ek7Pv7Wsjnh1/img.png?width=1024&amp;amp;height=714&amp;amp;face=0_0_1024_714&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/tech/harness-engineering-from-deep-insight/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://aws.amazon.com/ko/blogs/tech/harness-engineering-from-deep-insight/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBT442/dJMb88e4Il6/ypZMkV2tAIAFer8QyBJoOK/img.png?width=956&amp;amp;height=630&amp;amp;face=0_0_956_630,https://scrap.kakaocdn.net/dn/yD4bf/dJMb82eQ2CM/6Op4Y0R7gIXqfBK3hH3fnK/img.png?width=956&amp;amp;height=630&amp;amp;face=0_0_956_630,https://scrap.kakaocdn.net/dn/bP5UDa/dJMb88e4Il8/bozKhVkJX7Ek7Pv7Wsjnh1/img.png?width=1024&amp;amp;height=714&amp;amp;face=0_0_1024_714');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;하네스 엔지니어링으로 본 Deep Insight &amp;ndash; 로컬 개발에서 프로덕션 운영까지의 설계 여정 | Amazon Web&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI에게 단순히 &amp;ldquo;잘 해봐&amp;rdquo;라고 시키는 것과, AI가 스스로 만들고 평가하고 개선하는 Agentic 시스템을 설계하는 건 완전히 다른 결과물을 만들어냅니다. AWS Korea SA Team은 Agentic AI 시스템을 개발할&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;aws.amazon.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/49</guid>
      <comments>https://xeulbn-dev.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 11 May 2026 21:49:59 +0900</pubDate>
    </item>
    <item>
      <title>[CS Study] Kafka, 들어는 봤지만 잘 모르는 그거...</title>
      <link>https://xeulbn-dev.tistory.com/48</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에...&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 회사의 채용공고에서 기술 스택을 살펴보다 보면, 매번 빠지지 않고 등장하는 것이 있습니다. 바로 &lt;b&gt;Kafka&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;이번 글에서는 메시지 브로커가 무엇인지부터 시작해, RabbitMQ와는 어떻게 다른지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우아한 형제들이나 토스 같은 실제 서비스에서는 Kafka를 어떻게 활용하고 있는지까지 차근차근 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;왜 다들 Kafka, Kafka 하는 걸까?&quot;&lt;/b&gt; 라는 질문에 스스로 답을 찾아볼 수 있는 시간이 되었으면 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Message Broker란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 이야기를 하기 전에 먼저 짚고 넘어가야 할 개념이 있다. 바로 &lt;b&gt;메시지 브로커&lt;/b&gt;입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Message Broker는 Publisher로부터 전달받은 메시지를 Subscriber로 전달해주는 중간 역할이며, &lt;br /&gt;응용 소프트웨어 간에 메시지를 교환할 수 있게 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &lt;b&gt;메시지의 우체부&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher와 Subscriber 사이에서 메시지를 안전하게 전달해주는 중간자입니다.&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;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d62QRG/dJMcahc7G0p/ndYIgklW73P9GJjSgYA7w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d62QRG/dJMcahc7G0p/ndYIgklW73P9GJjSgYA7w0/img.png&quot; data-alt=&quot;Message Broker&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d62QRG/dJMcahc7G0p/ndYIgklW73P9GJjSgYA7w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd62QRG%2FdJMcahc7G0p%2FndYIgklW73P9GJjSgYA7w0%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Message Broker&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 브로커 안에는 보통 &lt;b&gt;Message Queue&lt;/b&gt;가 있고, 그 안에 여러 개의 &lt;b&gt;Topic&lt;/b&gt;이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher가 특정 Topic에 메시지를 보내면, 그 Topic을 구독하고 있는 Subscriber들이 메시지를 받아가는 구조입니다.&lt;/p&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;Publisher와 Subscriber가 서로 직접 연결될 필요가 없다&lt;/b&gt;는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher는 자기가 보낸 메시지를 누가 받는지, 몇 명이 받는지 신경 쓸 필요 없이 그냥 Topic에 던져넣기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscriber 입장에서도 메시지가 어디서 왔는지보다는 자기가 관심 있는 Topic에서 메시지를 꺼내 쓰기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 시스템 간의 &lt;b&gt;결합도가 낮아지면서&lt;/b&gt;, 한쪽이 잠시 죽어도 다른 쪽은 영향받지 않고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 몰려도 큐에 쌓아두고 천천히 처리할 수 있게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Message Broker의 종류&lt;/h4&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;b&gt;RabbitMQ&lt;/b&gt; - 전통적인 메시지 큐 시스템의 대표주자&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka&lt;/b&gt; - 분산 이벤트 스트리밍 플랫폼&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis&lt;/b&gt; - 인메모리 데이터 스토어이지만 Pub/Sub 기능 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Celery&lt;/b&gt; - Python 기반의 분산 태스크 큐&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 많이 비교되는 것이 RabbitMQ와 Kafka입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 메시지를 다룬다는 점은 같지만, 공식 문서에서 자기 자신을 소개하는 방식부터가 다른점을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. RabbitMQ vs. Kafka&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZpaYs/dJMcaaLQJSw/CPKRzHZ8U185IUfMOaMO7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZpaYs/dJMcaaLQJSw/CPKRzHZ8U185IUfMOaMO7K/img.png&quot; data-alt=&quot;Kafka 공식 홈페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZpaYs/dJMcaaLQJSw/CPKRzHZ8U185IUfMOaMO7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZpaYs%2FdJMcaaLQJSw%2FCPKRzHZ8U185IUfMOaMO7K%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;1298&quot; height=&quot;151&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka 공식 홈페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 자기 자신을 단순한 메시지 큐 브로커가 아닌 &lt;b&gt;이벤트 스트리밍 플랫폼&lt;/b&gt;이라고 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 메시지를 주고받는 것을 넘어서, 데이터 파이프라인을 만들고, 스트리밍 분석을 하고, 데이터를 통합하는 등의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 큰 범주에 있다는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-25 오전 10.43.53.png&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byNC8W/dJMcai362ea/BxoaC8mUjyHVaA7IgKQNK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byNC8W/dJMcai362ea/BxoaC8mUjyHVaA7IgKQNK1/img.png&quot; data-alt=&quot;RabbitMQ 홈페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byNC8W/dJMcai362ea/BxoaC8mUjyHVaA7IgKQNK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyNC8W%2FdJMcai362ea%2FBxoaC8mUjyHVaA7IgKQNK1%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;1165&quot; height=&quot;688&quot; data-filename=&quot;스크린샷 2026-04-25 오전 10.43.53.png&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RabbitMQ 홈페이지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 RabbitMQ는 자신을 &lt;b&gt;메시징 및 스트리밍 브로커&lt;/b&gt;라고 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;애플리케이션 간의 효율적이고 신뢰할 수 있는 통신&quot;인 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;메시지 큐 본연의 역할에 충실&lt;/b&gt;하다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 자기소개의 차이가 두 시스템이 추구하는 방향성을 잘 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 &quot;메시지를 잘 전달하는 것&quot;에 집중하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 &quot;데이터 스트림을 다루는 플랫폼&quot;이 되고자 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Kafka의 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 기본 구조는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZVLnw/dJMcabqsZIO/krX9uexnTka48M6VHO4pN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZVLnw/dJMcabqsZIO/krX9uexnTka48M6VHO4pN1/img.png&quot; data-alt=&quot;Kafka의 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZVLnw/dJMcabqsZIO/krX9uexnTka48M6VHO4pN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZVLnw%2FdJMcabqsZIO%2FkrX9uexnTka48M6VHO4pN1%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;1288&quot; height=&quot;916&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka의 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;가 메시지를 생산해 &lt;b&gt;Kafka Cluster&lt;/b&gt;에 보내면, 클러스터는 &lt;b&gt;Topic&lt;/b&gt; 단위로 메시지를 분류해 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Topic은 다시 여러 개의 &lt;b&gt;Partition&lt;/b&gt;으로 나뉘어 분산 저장되며, &lt;b&gt;Consumer&lt;/b&gt;가 이를 읽어가게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단순한 구조 위에서 Kafka는 다양한 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 주요 기능들을 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Kafka의 주요 기능들&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qCmUN/dJMcaf7qBHL/2k6MPXhFTvgKGMSdDti5kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qCmUN/dJMcaf7qBHL/2k6MPXhFTvgKGMSdDti5kk/img.png&quot; data-alt=&quot;출처 : https://techblog.woowahan.com/17386/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qCmUN/dJMcaf7qBHL/2k6MPXhFTvgKGMSdDti5kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqCmUN%2FdJMcaf7qBHL%2F2k6MPXhFTvgKGMSdDti5kk%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;692&quot; height=&quot;307&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;307&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://techblog.woowahan.com/17386/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-1. Messaging (메시징)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 가장 기본적인 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka는 기존의 전통적인 메시지 브로커(예: ActiveMQ, RabbitMQ)를 대체하는 데 적합&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 브로커는 데이터 Producer와 Consumer간의 처리 단계를 분리하거나,&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;Kafka가 기존 메시지 브로커보다 강력한 이유는 다음 네 가지를 모두 제공하기 때문입니다.&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;높은 처리량(Throughput)&lt;/b&gt;: 초당 수백만 건의 메시지 처리 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내장된 파티셔닝(Partitioning)&lt;/b&gt;: 토픽을 여러 파티션으로 나눠 병렬 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복제(Replication)&lt;/b&gt;: 데이터를 여러 브로커에 복제해 안정성 확보&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내결함성(Fault-Tolerance)&lt;/b&gt;: 일부 브로커가 죽어도 시스템은 계속 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지가 한 세트로 묶이면서 &lt;b&gt;대규모 메시지 처리 시스템에 매우 적합&lt;/b&gt;한 솔루션이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-2. Website Activity Tracking (웹사이트 활동 추적)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-25 오전 10.59.16.png&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2LQbT/dJMcagSPzBb/5mGUoZV6Xfc3odkiKj7HFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2LQbT/dJMcagSPzBb/5mGUoZV6Xfc3odkiKj7HFk/img.png&quot; data-alt=&quot;Kafka 공식 문서 (출처 : https://kafka.apache.org/uses/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2LQbT/dJMcagSPzBb/5mGUoZV6Xfc3odkiKj7HFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2LQbT%2FdJMcagSPzBb%2F5mGUoZV6Xfc3odkiKj7HFk%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;984&quot; height=&quot;257&quot; data-filename=&quot;스크린샷 2026-04-25 오전 10.59.16.png&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka 공식 문서 (출처 : https://kafka.apache.org/uses/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka가 처음 LinkedIn에서 만들어진 이유 중 하나가 바로 이것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트에서 사용자가 발생시키는 &lt;b&gt;페이지 조회(Page View), 검색, 클릭 등의 활동 정보를 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활동 유형별 Topic에 실시간으로 Publish&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;/li&gt;
&lt;li&gt;실시간 모니터링 (예: 이상 트래픽 감지)&lt;/li&gt;
&lt;li&gt;Hadoop이나 데이터 웨어하우스로의 적재 및 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 포인트는 &lt;b&gt;사용자 활동 데이터는 페이지마다 여러 개의 이벤트가 발생하기 때문에 매우 높은 트래픽을 발생시킨다&lt;/b&gt;는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도 트래픽을 안정적으로 받아내려면 결국 Kafka 같은 시스템이 필요해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-3. Metrics (메트릭 수집)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;AIDrawing_260425_76be6ab2-87f5-490a-8fae-bf30623cb2cc_0_MiriCanvas.png&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEnu2V/dJMcaaZmoYd/VhkT0Srd95fjd6AIi1shw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEnu2V/dJMcaaZmoYd/VhkT0Srd95fjd6AIi1shw1/img.png&quot; data-alt=&quot;Kafka Metrics&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEnu2V/dJMcaaZmoYd/VhkT0Srd95fjd6AIi1shw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEnu2V%2FdJMcaaZmoYd%2FVhkT0Srd95fjd6AIi1shw1%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;1886&quot; height=&quot;1024&quot; data-filename=&quot;AIDrawing_260425_76be6ab2-87f5-490a-8fae-bf30623cb2cc_0_MiriCanvas.png&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka Metrics&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka는 운영 모니터링 데이터(operational monitoring data) 수집에도 자주 사용&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서 운영 중인 수많은 애플리케이션에서 발생하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 통계 데이터(CPU 사용률, 응답 시간, 에러 카운트 등)를 중앙으로 모아 운영 상태 피드를 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서버에서 발생하는 메트릭을 Kafka로 일원화하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Datadog 같은 모니터링 툴이나 Grafana 같은 대시보드로 자연스럽게 연결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-4. Log Aggregation (로그 수집)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka는 기존의 로그 수집 시스템(Scribe, Flume 등)을 대체할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 로그 수집은 서버의 물리적 로그 파일을 모아 중앙 스토리지(HDFS 등)에 저장하는 방식인 반면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 &lt;b&gt;로그를 메시지 스트림(stream)으로 추상화&lt;/b&gt;하여 더 간결하고 실시간 처리에 적합한 구조를 제공해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 다음과 같은 장점을 갖습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;낮은 지연 시간&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능은 기존 로그 시스템과 유사하지만, &lt;b&gt;복제 기반의 내구성 보장&lt;/b&gt;과 &lt;b&gt;낮은 end-to-end 지연 시간&lt;/b&gt; 면에서 더 우수합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-5. Stream Processing (스트림 처리)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 Kafka 사용자들은 데이터를 여러 단계로 처리하는 &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-filename=&quot;ChatGPT Image 2026년 4월 25일 오전 11_24_28.png&quot; data-origin-width=&quot;1969&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKfF1i/dJMcaf0EBQI/WCyTKq1gKUu82UoiPKLkk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKfF1i/dJMcaf0EBQI/WCyTKq1gKUu82UoiPKLkk1/img.png&quot; data-alt=&quot;Kafka Strema Processing Pipeline in News Recommendation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKfF1i/dJMcaf0EBQI/WCyTKq1gKUu82UoiPKLkk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKfF1i%2FdJMcaf0EBQI%2FWCyTKq1gKUu82UoiPKLkk1%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;1969&quot; height=&quot;799&quot; data-filename=&quot;ChatGPT Image 2026년 4월 25일 오전 11_24_28.png&quot; data-origin-width=&quot;1969&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka Strema Processing Pipeline in News Recommendation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 뉴스 추천 시스템을 생각해보겠습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;RSS 피드에서 기사 데이터를 수집 &amp;rarr; articles 토픽에 게시&lt;/li&gt;
&lt;li&gt;중복 제거 및 정제 &amp;rarr; cleaned_articles 토픽에 게시&lt;/li&gt;
&lt;li&gt;사용자 맞춤 추천 생성 &amp;rarr; recommendations 토픽에 게시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Kafka는 &lt;b&gt;여러 토픽이 연결된 실시간 데이터 플로우 그래프&lt;/b&gt;를 형성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 단계의 출력이 다음 단계의 입력이 되는 식으로 자연스럽게 파이프라인이 만들어지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작업을 더 쉽게 만들어주는 경량 스트림 처리 라이브러리로 &lt;b&gt;Kafka Streams&lt;/b&gt;가 포함되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Apache Storm, Apache Samza 같은 대안 오픈소스도 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(여기서 잠깐) Kafka Streams란?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0sUZf/dJMcaiXmfYO/Tko4h6S9TQqNraPkN4kHP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0sUZf/dJMcaiXmfYO/Tko4h6S9TQqNraPkN4kHP1/img.png&quot; data-alt=&quot;Kafka Streams 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0sUZf/dJMcaiXmfYO/Tko4h6S9TQqNraPkN4kHP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0sUZf%2FdJMcaiXmfYO%2FTko4h6S9TQqNraPkN4kHP1%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;1678&quot; height=&quot;572&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka Streams 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실시간 애플리케이션과 마이크로서비스를 작성하는 가장 쉬운 방법&lt;/b&gt;으로 소개되는 Kafka Streams는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 및 출력 데이터가 Kafka 클러스터에 저장되는 클라이언트 라이브러리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 측에서 표준 Java 및 Scala 애플리케이션을 작성하고 배포하는 간편함과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 서버 측 클러스터 기술의 이점을 결합한다는 점이 특징입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-6. Event Sourcing&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Sourcing은 어플리케이션의 상태 변화를 시간 순서대로 기록된 이벤트 로그로 저장하는 설계 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 DB가 현재 상태만을 저장한다면, 이벤트 소싱은 어떤 변화가 일어났는지의 모든 기록을 저장합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2026년 4월 25일 오전 11_27_45.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zvxUW/dJMcah5hteM/QboasqQgPyURebl7kZsrhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zvxUW/dJMcah5hteM/QboasqQgPyURebl7kZsrhK/img.png&quot; data-alt=&quot;Kafka EventSourcing 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zvxUW/dJMcah5hteM/QboasqQgPyURebl7kZsrhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzvxUW%2FdJMcah5hteM%2FQboasqQgPyURebl7kZsrhK%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;1536&quot; height=&quot;1024&quot; data-filename=&quot;ChatGPT Image 2026년 4월 25일 오전 11_27_45.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kafka EventSourcing 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 은행 계좌의 잔고가 100만원이라는 정보만 저장하는 게 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;5만원 입금&quot;, &quot;10만원 출금&quot;, &quot;이자 지급&quot;처럼 변화의 과정 전체를 이벤트로 저장합니다.&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;Kafka는 대용량 로그 데이터를 장기 저장할 수 있으므로 이벤트 소싱 패턴의 백엔드로 훌륭하게 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic 자체가 이미 append-only log 구조이기 때문에, 이벤트 소싱과 자연스럽게 잘 맞아떨어집니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 실제 기업 사례로 보는 Kafka&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 한국의 대표적인 IT 기업들은 Kafka를 어떻게 쓰고 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6-1. 우아한 형제들&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한 형제들은 배달 이벤트를 Kafka로 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lPGd7/dJMcaaZmoGT/b6t9kLuQkN62ECvNfVifkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lPGd7/dJMcaaZmoGT/b6t9kLuQkN62ECvNfVifkK/img.png&quot; data-alt=&quot;우아한 형제들의 Kafka 사용 방식 (출처 : https://techblog.woowahan.com/17386/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lPGd7/dJMcaaZmoGT/b6t9kLuQkN62ECvNfVifkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlPGd7%2FdJMcaaZmoGT%2Fb6t9kLuQkN62ECvNfVifkK%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;662&quot; height=&quot;422&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우아한 형제들의 Kafka 사용 방식 (출처 : https://techblog.woowahan.com/17386/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 서버에서는 배달 이벤트를 수신한 후 &lt;b&gt;전처리 과정을 거쳐 조회하기 편한 형태로 가공&lt;/b&gt;해 분석 토픽으로 이벤트를 재발행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 이벤트를 가공하여 분석할 수 있도록 또 다른 토픽과 스트림으로 생성하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적이 다르기에 원본 토픽과 분석용 토픽을 분리해 사용&lt;/b&gt;하는 것이었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 처리량과 리소스 차이&lt;/b&gt;: 서비스 토픽과 분석용 토픽은 서로 다른 처리량과 리소스가 필요하므로, &lt;br /&gt;토픽과 서버를 분리해 특성에 맞는 리소스를 사용하고 조정할 수 있도록 구성했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애 영향 범위 분리&lt;/b&gt;: 주요한 서비스 로직에 사용되는 토픽과 분석에 사용되는 토픽은 문제가 발생하더라도 영향 범위를 분리해 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 토픽 안에서는 latest-delivery(key: 배달 식별자, value: 배달 정보)와 count-per-status(key: 배달 상태, value: 정수) 같은 상태 저장소를 만들어 실시간 집계도 가능하게 해놓은 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6-2. Toss&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스는 대규모 트래픽과 멀티 데이터센터 환경에서 Kafka를 운영하는 사례를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 트래픽을 50:50으로 두 데이터센터에 분산시키고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 데이터센터의 K8s 클러스터에서 Producer/Consumer가 동작하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터 1과 클러스터 2 사이에 &lt;b&gt;데이터 미러링과 Offset Sync&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;397&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceJA9W/dJMcaiwjbFm/05WwBi6sk6tLVFIOZQuiO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceJA9W/dJMcaiwjbFm/05WwBi6sk6tLVFIOZQuiO0/img.png&quot; data-alt=&quot;Toss에서의 Kafka 활용 (출처 :https://toss.tech/article/kafka-distribution-1)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceJA9W/dJMcaiwjbFm/05WwBi6sk6tLVFIOZQuiO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceJA9W%2FdJMcaiwjbFm%2F05WwBi6sk6tLVFIOZQuiO0%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;397&quot; height=&quot;483&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Toss에서의 Kafka 활용 (출처 :https://toss.tech/article/kafka-distribution-1)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 통해 아래와 같은 장점을 얻은 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발자 입장에서의 투명성&lt;/b&gt;: 서비스 개발자는 어느 데이터센터에 서비스가 배포되는지 신경 쓸 필요가 없습니다.&lt;br /&gt;데이터센터 장애 상황 시 안전한 데이터센터로 트래픽만 옮겨주면 정상적인 Kafka가 모든 메시지를 받게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터 분리를 통한 운영 효율&lt;/b&gt;: 하나의 대규모 클러스터를 N개의 작은 클러스터로 분리했습니다. &lt;br /&gt;이를 통해 각 Connector 잡의 Task 수를 유연하게 조정할 수 있게 됐고, 적절한 Task 분배로 기존 대비 성능을 향상시켰습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 안정성과 편의성 향상&lt;/b&gt;: 클러스터가 너무 많은 Connector 잡을 스케줄링하지 않아도 되어 부담이 줄어들고, &lt;br /&gt;N개로 분리된 클러스터를 부분적으로 작업할 수 있어서 운영 안정성과 편의성이 크게 향상되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 트래픽 환경에서 단일 클러스터의 한계를 넘어서기 위한 실용적인 접근 방식을 확인할 수 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7. 그래서 Kafka 왜 쓸까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 메시지 큐 그 이상의 가치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ를 비롯한 다양한 메시지 브로커도 있지만, Kafka는 단순히 메시지를 전달해주는 메시지 큐의 역할만 하는 것이 아닙니다.&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;많은 대기업들이 Kafka를 선택하게 되는 것 같습니다.&lt;/p&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;2) 대규모 트래픽에 강한 클러스터 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka를 쓰면 &lt;b&gt;대규모 트래픽 상황에서도 수많은 브로커들을 하나의 클러스터로 묶어서&lt;/b&gt; 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 트래픽이 늘어나면 브로커를 추가해 수평 확장하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대규모 트래픽이 발생하는 대기업 환경에서 충분히 메리트가 있는 선택인 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Kafka의 매력은 &lt;b&gt;메시지 전달 + 데이터 저장 + 스트리밍 처리 + 확장성 &lt;/b&gt;이라는 네 마리 토끼를 한 번에 잡는 것에 있는 것 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 모든 서비스에 Kafka가 정답인 것은 아니라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 규모의 서비스라면 RabbitMQ나 Redis Pub/Sub만으로도 충분할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 작업 큐 용도라면 Celery가 더 적합할 수 있다.&lt;/p&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;지금 내가 풀어야 할 문제가 무엇인가&quot;&lt;/b&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;여러 컨슈머가 같은 데이터를 다른 목적으로 활용해야 한다면 그때 Kafka는 정말 좋은 선택지가 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/48</guid>
      <comments>https://xeulbn-dev.tistory.com/48#entry48comment</comments>
      <pubDate>Sat, 25 Apr 2026 11:28:12 +0900</pubDate>
    </item>
    <item>
      <title>[회고와 생각] Agile이란 무엇인가? Sprint란 무엇인가? (feat. DDD 동아리를 하며)</title>
      <link>https://xeulbn-dev.tistory.com/47</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 DDD 동아리에서 프로젝트를 진행하게 되며 저희 팀에서 강조한 가치는 딱 두가지였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Agile하게.&lt;br /&gt;Sprint.&lt;/span&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;이게 맞는가? 이렇게 해도 되는가?에 대한 고민이 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에, Agile과 Sprint에 대해 고민을 많이 해왔던 순간들과 저의 감정들을 기록해보려합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본문&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Agile이란?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보처리 기사를 공부해보셨거나, 이 IT업계에서 종사를 하게 되면 정의 정도는 어디선가 들어본 적이 있으실 것이라 생각합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Agile이란 소프트웨어 개발 방법론의 하나로,&amp;nbsp;&lt;br /&gt;작업 계획을 짧은 단위로 세우고 제품을 만들고 고쳐나가는 방법을 반복함으로써 고객의 요구 변화에&lt;br /&gt;유연하고도 신속하게 대처하자는 점에서 나오는 방법론입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정의 어디선가 많이 들어보셨을 것이라 생각합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceRWIy/dJMcaaLLZW9/i9MA6FYalh9kGxNKAkZaS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceRWIy/dJMcaaLLZW9/i9MA6FYalh9kGxNKAkZaS1/img.png&quot; data-alt=&quot;CodeStates 이미지 활용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceRWIy/dJMcaaLLZW9/i9MA6FYalh9kGxNKAkZaS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceRWIy%2FdJMcaaLLZW9%2Fi9MA6FYalh9kGxNKAkZaS1%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;654&quot; height=&quot;302&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CodeStates 이미지 활용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 짧은 스프린트로 많은 사이클을 돌게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 취준생이자 주니어 개발자인 저의 관점에서 저는 사실 여태껏 애자일하게 프로젝트를 진행해왔다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디자인이 나오면, 개발을 진행했고(이때의 개발은 가볍게 MVP코드만 빠르게 뽑았습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 대한 부하테스트까지 한 사이클로 돌아가도록 진행했었으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이번에 만나게 된 팀에서 강조한 애자일에 대해 듣게 되면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여태까지 진행한 애자일 방법론은 애자일이 아니었구나를 다시금 느끼게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획조차도 애자일해야하며 유저의 반응(데이터)에 맞춰서 문제지점을 파악하고, 수정해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 2001년에 발표된 Agile Manifesto를 보면, 그 핵심 가치는 다음과 같이 정리됩니다.&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;br /&gt;포괄적인 문서보다 &lt;b&gt;작동하는 소프트웨어&lt;/b&gt;를,&lt;br /&gt;계약 협상보다 &lt;b&gt;고객과의 협력,&lt;br /&gt;&lt;/b&gt;계획을 따르기보다 &lt;b&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;여기서 주목해야 할 부분은 마지막 문장입니다. &quot;계획을 따르기보다 변화에 대응하기를.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말은 곧, &lt;b&gt;우리가 세운 계획은 틀릴 수 있다는 것을 전제로 한다&lt;/b&gt;는 뜻입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 우리는 그 &quot;틀린 계획&quot;을 어떻게 빠르게 발견하고, 어떻게 고쳐나갈 것인가?&lt;/span&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;이 질문에 대한 답이 바로 Agile이고, Sprint인 것이죠.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 왜 '실패'가 Agile의 핵심일까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 &quot;Fail Fast, Fail Often&quot;이라는 말을 들어보셨을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직역하면 &quot;빠르게 실패하고, 자주 실패하라&quot;는 다소 도발적인 문장입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이 문장을 접했을 때, 저는 의문이 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자가 실패하는 게 좋은 일이라고? &lt;br /&gt;그래도 일은 자고로 한 번에 완벽하게 하는게 좋은게 아닌가? &lt;br /&gt;왜 실패해야하지?&lt;/span&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;b&gt;아무리 완벽하게 만들더라도 시대가 빠르게 변하는 세상에서 사용자의 요구사항 변화는 매우 빠르게 일어납니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1개월 전의 요구사항이 1개월 후에는 변할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Waterfall(폭포수) 방식으로 6개월을 풀로 개발한 후 &quot;사용자가 원하지 않는 제품이었다&quot;는 걸 발견하면,&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;반면 Agile에서는 2주짜리 Sprint를 돌면서 쌓이는 데이터로 사용자와 소통합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;예를 들어, 사용자가 몇시 시간대에 가장 많이 접속하였고, &lt;br /&gt;해당 서비스를 주기적으로 몇일 동안 사용하다가 그만 사용하였다고 해보겠습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터로부터 우리는 교훈을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자들이 처음에는 우리 서비스를 잘 썼는데, 어느 순간 이후, 일정 기간 이후부터는 사용하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜일까? 우리 서비스의 리텐션이 부족한가? 아니면, 사용감에 불편함이 있는가? 등등 다양한 가설을 세우고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 빠르게 배포하는 과정을 통해 검증을 해나갈 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 데이터 기반 의사결정: 실패를 '학습'으로 바꾸는 장치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀은 각 Sprint가 끝날 때마다 다음과 같은 루프를 돕니다.&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1776997731094&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[가설] &amp;rarr; [빠른 구현] &amp;rarr; [사용자에게 노출] &amp;rarr; [데이터 수집] &amp;rarr; [학습] &amp;rarr; [다음 가설]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 중요한 건 가설입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 기능을 넣으면 리텐션이 10% 올라갈 것이다&quot; ,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;사용자 20명 중 15명은 지속해서 서비스를 사용할 것이다&quot; 는 가설이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 기능을 넣으면 좋겠지&quot; 는 그냥 감입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가설은 검증 가능해야 하고, 검증 가능하다는 말은 곧 &lt;b&gt;틀릴 수 있다는 뜻&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 좋은 Agile 팀은 실패를 잘못된 것으로 보지 않고, &lt;b&gt;가설이 기각된 유의미한 결과&lt;/b&gt;로 받아들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과학에서 실험이 기각되어도 그건 실패가 아니라 발견이듯이, 제품에서도 마찬가지입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 내가 오해했던 Agile, 진짜 Agile을 만나고 나서&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말씀드렸듯이, 저는 이번 DDD를 만나기 전까지 스스로가 꽤 애자일하게 일해왔다고 믿고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각했던 애자일은 이런 모습이었습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1776997711129&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[디자인 완성] &amp;rarr; [MVP 빠르게 구현] &amp;rarr; [부하 테스트] &amp;rarr; [개선] &amp;rarr; 다음 기능&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 주기로 개발을 쪼개서 빠르게 돌리고, 테스트하고, 고치는 것. 이게 애자일이 아니면 뭐가 애자일인가? 라고 생각했었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 사이클을 다시 곰곰이 들여다보면, 치명적인 결함이 하나 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;디자인이 이미 완성된 후부터 사이클이 시작된다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 제가 했던 애자일은 &lt;b&gt;개발 단계에서만 애자일&lt;/b&gt;했던 거였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획과 디자인은 여전히 Waterfall이었고, 그 결과물을 그대로 받아서 구현만 한 셈이었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;진짜 애자일은&amp;nbsp; 기획에서부터 시작된다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD동아리를 하며 만난 팀에서의 애자일은 달랐습니다. 사이클의 시작점 자체가 달랐습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776997830769&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[가설 수립] &amp;rarr; [최소한의 기획] &amp;rarr; [빠른 구현] &amp;rarr; [사용자 반응(데이터) 수집]
   &amp;darr;
[가설 검증/기각] &amp;rarr; [다음 가설 수립] &amp;rarr; ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 가장 충격적이었던 부분은 &lt;b&gt;&quot;기획조차도 애자일해야 한다&quot;&lt;/b&gt; 는 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 그동안 기획을 &quot;정답&quot;이라고 여겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM이나 기획자가 내려준 기획서는 완성된 설계도이고, 개발자는 그걸 잘 구현하면 된다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획이 바뀌면? &quot;아, 기획이 또 바뀌네...&quot; 하고 한탄하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 진짜 애자일에서는 &lt;b&gt;기획은 자주 바뀌는 게 정상&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니, 더 정확히 말하면 &lt;b&gt;기획은 원래 실패할 수 있는 것&lt;/b&gt;이고, 사용자 데이터를 만나면서 점점 맞춰가는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전제를 받아들이고 나니, 기획 변경에 대한 제 태도도 완전히 바뀌었습니다.&lt;/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;p data-ke-size=&quot;size16&quot;&gt;이 전환은 개발자의 역할에 대한 생각도 바꿔놓았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전의 저는 잘 짜여진 기획을 견고한 코드로 구현하려는 사람이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 견고함, 완성도, 확장성에 집착했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 쓸데없이 많은 레이어를 쌓고, 혹시 모를 미래를 대비해 추상화를 과하게 하고 있었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 진짜 애자일 팀에서의 개발자는 달랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;가설을 검증 가능한 형태로 가장 빠르게 만드는 사람&quot;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면, 한 번도 사용자에게 노출되지 않은 가설은 아무리 아름답게 설계되어 있어도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아직 쓸모가 증명되지 않은 코드&lt;/b&gt;이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버려질 가능성이 있는 코드에 완벽주의를 쏟아붓는 건, 엄밀히 말하면 리소스 낭비입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 이제 코드를 짤 때 이런 질문을 먼저 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 기능은 지금 이 가설을 검증하기 위해 필요한가? 아니면 혹시 모를 미래를 위해 만드는가?&quot;&lt;br /&gt;&quot;과한 설계는 아닐까? 과잉 설계와 확장성 사이의 선을 잘 정해놓고 가고 있는가?&quot;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 실패를 두려워하지 않는다는 것의 의미&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실패가 두려웠던 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직하게 고백하자면, 저는 실패가 정말 싫었습니다. 정확히는, 실패로 인해 받게 될 &lt;b&gt;평가&lt;/b&gt;가 두려웠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기획한 기능이 사용자에게 외면받으면서 유령 서비스가 되어갈까봐&lt;/li&gt;
&lt;li&gt;빠르게 짠 MVP가 지저분하면 &quot;코드 품질이 별로네&quot; 소리를 들을까 봐&lt;/li&gt;
&lt;li&gt;가설이 틀리면 저 자체가 부정당하는 느낌이 들었기에&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 두려움 때문에 저는 무의식적으로 &lt;b&gt;실패하지 않으려고, 처음부터 완벽하려고 헀습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실한 것, 검증된 것, 이미 남들이 해본 것, 학습 목적으로 하는 것. 그러니까 결국 새로운 시도를 피하고 있었던 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 사실 저렇게 실패하지 않으려고, 처음부터 완벽하려고 쌓아올리더라도 완벽할 수 없었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DDD 팀에서 처음 마주한 &quot;실패해도 괜찮다&quot;는 문화&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD 팀에 합류해서 가장 낯설었던 건, 회고 시간에 나오는 대화였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 실패를 통해 우리는 무엇을 배웠을까요?&lt;/li&gt;
&lt;li&gt;좋아요. 이런 경험을 통해 저희는 이게 안된다는 것을 한 가지 깨우쳤네요.&lt;/li&gt;
&lt;li&gt;실패한다고 생각하고 빠르게 가장 중요한 메인 기능부터 빠르게 시작해야합니다.&lt;/li&gt;
&lt;li&gt;가설을 좀 더 명확하고 날카롭게 정의해볼까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패를 두려워 하지 않는 것을 계속해서 강조했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 &lt;b&gt;&quot;실패했다. 여기서 어떤 것을 얻어서 문제를 어떻게 해결할 수 있을까?&quot;&lt;/b&gt; 라고만 표현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 곧바로 &lt;b&gt;&quot;그래서 다음엔 어떻게 보완할 수 있을까?&quot;&lt;/b&gt; 로 넘어갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 어색했습니다. 속으로 5조5억번 정도는 '이게 맞나? 이렇게 했다가는 사용자 1명도 없을 것 같은데...'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 팀에 자주 질문하기도 했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;진짜 이대로 괜찮을까요? , 사용자 한 명도 없을 것 같은데요...&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 몇 번의 Sprint를 반복하면서, 저는 이 문화의 힘을 서서히 이해하게 됐습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&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;오히려 정반대입니다.&lt;/p&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;DDD 팀의 회고에서는 늘 이런 질문이 따라붙었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왜 이 가설을 세웠는가?&lt;br /&gt;어떤 데이터로 검증했는가?&lt;br /&gt;기각된 원인은 무엇이라고 추정하는가?&lt;br /&gt;이 경험을 다음 Sprint에 어떻게 반영할 것인가?&lt;br /&gt;우리 서비스는 왜 실패했는가?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문들을 통과해야만, 실패가 비로소 &lt;b&gt;학습 자산&lt;/b&gt;으로 쌓이고 이 실패들이 토대가 되어 성공으로 이어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 &quot;실패했네요, 다음엔 잘하자&quot; 하고 넘어가는 건 애자일이 아니라 그냥 무책임에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실패를 두려워하지 않는 팀은 &lt;b&gt;실패를 허용하는 팀&lt;/b&gt;이 아니라, &lt;b&gt;실패를 빠르게 자산으로 바꿀 수 있는 시스템을 갖춘 팀&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그리고 저에게 남은 변화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 제 안에서 달라진 것들을 정리해보면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;처음부터 잘 만들어야 한다는 강박이 있었는데, 이제는 &quot;일단 검증 가능한 형태로 내놓자&quot; 가 먼저입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽함은 사용자 데이터가 쌓인 후에 추구해도 늦지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째, 실패를 공유하는 게 덜 무서워졌습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전엔 실패를 감추고 싶었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제는 &quot;이번 가설 기각됐어요. 이유는 이러이러한 것 같아요&quot; 라고 먼저 꺼내게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패를 팀에 공유해야 팀 전체가 학습하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋째, &quot;내 생각&quot;이 아니라 &quot;데이터&quot;로 말하는 습관이 생겼습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;제 생각엔 이게 더 좋아 보여요&quot; 보다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지난번 가설 결과를 보면 이 방향이 더 가능성이 있어 보여요&quot; 라고 말하는 쪽이 훨씬 설득력이 있더군요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실패를 두려워했던 나, 그리고 지금&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DDD에 합류하기 전의 저는, &quot;실패하지 않는 것&quot;이 곧 실력이라고 믿었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음부터 완벽하려고 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 프로젝트를 통해 제가 배운 가장 큰 교훈은 이것입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;실패하지 않는 사람&quot;이 실력자가 아니라, &quot;실패로부터 가장 빨리 배우는 사람&quot;이 실력자다.&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;Agile도, Sprint도, 결국 이 한 문장을 실현하기 위해 존재하는 도구라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧게 쪼개고, 빠르게 내놓고, 사용자에게 두들겨 맞고, 거기서 배운 걸로 다시 쪼개고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 끝없는 루프를 감당하기 위해 필요한 건 뛰어난 재능이 아니라, &lt;b&gt;실패를 두려워하지 않는 마음가짐&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직도 저는 실패가 두렵습니다. 사람인 이상 완전히 없어지지는 않을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이제는 그 두려움을 다루는 방법을 조금은 알 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패를 피하려 하지 말고, 실패에서 무엇을 배울지를 먼저 설계하자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로의 프로젝트에서도 저는 계속 실패할 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 실패 하나하나가 저를 더 단단하게 만들어 줄 거라 믿습니다.&lt;/p&gt;</description>
      <category>Backend</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/47</guid>
      <comments>https://xeulbn-dev.tistory.com/47#entry47comment</comments>
      <pubDate>Fri, 24 Apr 2026 12:08:10 +0900</pubDate>
    </item>
    <item>
      <title>[DetoxMate] MVP개발 1차 개발 일기 (feat. TDD)</title>
      <link>https://xeulbn-dev.tistory.com/46</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 DDD 동아리에서 프로젝트를 진행하며 진행하는 과정들을 기록해보기 위해서 이 글을 다시 작성하기 시작하려합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트의 주제는 Digital Detox로, 실제로 Digital Detox에 대해 필요성을 느끼고 있는 현대인이 많을 것이고 (저를 포함해서...),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기위해 어떻게 하면 그 중독성 있는 릴스를 끊을 수 있을 것인가에 중점을 맞춰 프로젝트를 진행하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스는 실서비스로 배포가 될 예정인지라, 자세한 기획 내용까지 전부 밝히기는 어려운 점 양해부탁드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 자체가 굉장히 애자일한 방식으로 스프린트하게 흘러가기 때문에(사용자의 반응과 데이터를 바탕으로 기능 및 세부 구현이 변경됩니다.)&amp;nbsp; 따로 도메인을 처음부터 어디하자 어디하자가 아니라, 진행하면서 빠르게 개발되어야할 부분들을 정하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획에서 생각하기에 중요하다고 생각되어지는 부분부터 개발을 하는 방식으로 flow가 흘러갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 우선은 알림을 맡게 되었고, 개발 진행은 TDD를 기반으로 진행하기로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD란, Test를 먼저 작성하고 이후 서비스코드들이 작성되는 방식으로 Test가 중심이 되어 개발되는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 이번 1차 개발하며 느낀 점을 작성해보고자 이 글을 작성합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. TDD를 시작하며.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음으로 TDD를 적용해보기로 했고, 실제로 처음부터 테스트 먼저 짜려고 하니 어안이 벙벙했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 테스트부터 어떻게 짜는게 맞을지도 감이 안잡히고, 도메인 코드조차 없는 상태에서 짜는 테스트는 눈앞이 캄캄했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 맡은 알림 파트만 하더라도 엔티티가 Notification, NotificationHistory, Notification Type 이렇게 세 가지 엔티티가 있었는데,&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;과연 Notification, Notification_history, Notification_type 이 세 엔티티 중 &lt;br /&gt;어디의 코드가 먼저 작성되어야할까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 도메인의 설계의도를 기반으로 고민해봤습니다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;notification :&lt;/b&gt; 알림 템플릿 (&quot;{nickname}님이 반응을 남겼습니다&quot;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;notification_type :&amp;nbsp;&lt;/b&gt;알림 종류 (CERTIFICATION, COMMENT, REACTION etc.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;notification_history :&lt;/b&gt; 실제 발송된 알림의 스냅샷 (&quot;홍길동 님이 댓글을 남겼습니다&quot;) + 수신자 ID(user_id)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 의도를 가진 도메인이라고 생각을 하였을때, 테스트의 순서가 어떻게 될지를 고민해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notification_history를 먼저 테스트하려해도 notification에 대한 history이기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notification 자체가 구현에 있어 중간에 걸림돌(결국 다시 notification_history작성을 하러 가야할 것)이 될 것이라고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 notification을 먼저 테스트하기에는, 결국 notification을 테스트하다보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;notification_type 테스트 코드를 작성하러 가야만 합니다. (notification : notification_type이 n:1이기 때문에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이리하여, notification_type에 대한 test를 먼저 작성하기로 했습니다.&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;notification_type test 코드 작성 (컴파일 에러 발생시키기)&lt;/li&gt;
&lt;li&gt;notification_type Domain 코드 작성 (최소 구현)&lt;/li&gt;
&lt;li&gt;이후 다시 notification_type text 실행 (테스트 전부 통과 확인)&lt;/li&gt;
&lt;li&gt;이후 해당 도메인 리팩토링&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 모든 Layer마다 반복적으로 진행하면 되는 것입니다.&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;2. notification_type test코드 작성으로 컴파일 에러 발생시키기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 테스트를 먼저 짜보면 짜볼수록, 저 빨간색이 너무 눈에 거슬렸습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJtKpI/dJMcafsIoh0/FamGt9M4fNUzUfBBlceuzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJtKpI/dJMcafsIoh0/FamGt9M4fNUzUfBBlceuzk/img.png&quot; data-alt=&quot;NotificationTypeTest 구현&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJtKpI/dJMcafsIoh0/FamGt9M4fNUzUfBBlceuzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJtKpI%2FdJMcafsIoh0%2FFamGt9M4fNUzUfBBlceuzk%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;1042&quot; height=&quot;543&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NotificationTypeTest 구현&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 없는 것을 테스트로 작성하려니 코드를 작성하면서도 계속해서 빨간 오류가 뜨게되는 것이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 돌려본 결과 너무나 당연하게도 아래와 같이 오류가 주루룩 나왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ssPtf/dJMcahD3lqQ/I3FnV3hgV2b3BtKrCmhQy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ssPtf/dJMcahD3lqQ/I3FnV3hgV2b3BtKrCmhQy0/img.png&quot; data-alt=&quot;Test가 실제로 실패하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ssPtf/dJMcahD3lqQ/I3FnV3hgV2b3BtKrCmhQy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FssPtf%2FdJMcahD3lqQ%2FI3FnV3hgV2b3BtKrCmhQy0%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;813&quot; height=&quot;405&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Test가 실제로 실패하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지를 하나하나 보면서 직접 코드를 이제 작성해나가봐야겠다라고 생각하였습니다.&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;3. notification_type 도메인 코드 작성 (feat. 많은 생각을 하게 되는...)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테스트 에러 중 가장 먼저 보이는 에러는 CERTIFICATION 즉, notification_type의 Enum값 자체가 없어서 발생하는 오류였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 NotificationTypeCode Enum값들 먼저 정돈해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum값들은 확장 가능하도록, 우선은 필수적으로 들어갈만한 값들만 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증을 하면 가는 알림인 인증 (CERTIFICATION), 댓글달면 가는 알림인 댓글(COMMENT), 반응을 달면(좋아요 등) 나오는 알림 반응(REACTION)정도를 Notification Type으로 정의하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNlkTj/dJMcacCPtvL/2ExrAXRrxwnBkBhSgoOobk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNlkTj/dJMcacCPtvL/2ExrAXRrxwnBkBhSgoOobk/img.png&quot; data-alt=&quot;NotificationType Enum&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNlkTj/dJMcacCPtvL/2ExrAXRrxwnBkBhSgoOobk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNlkTj%2FdJMcacCPtvL%2F2ExrAXRrxwnBkBhSgoOobk%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;285&quot; height=&quot;111&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NotificationType Enum&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 바로 아래 에러를 확인하면 NotificationType 쪽 create메소드가 없어서 나오는 에러가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 Notification_type쪽 도메인 코드 작성과 create메소드 작성을 진행했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/poVDo/dJMcafzvDwS/qGBrlZrcVvdKhczOApgHh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/poVDo/dJMcafzvDwS/qGBrlZrcVvdKhczOApgHh0/img.png&quot; data-alt=&quot;NotificationType 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/poVDo/dJMcafzvDwS/qGBrlZrcVvdKhczOApgHh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpoVDo%2FdJMcafzvDwS%2FqGBrlZrcVvdKhczOApgHh0%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;898&quot; height=&quot;661&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NotificationType 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 하나하나 에러를 먼저 보면서 작성을해보니, notification과 notification_type간의 연관관계에 대해서도 다시금 생각해보게 되었습니다. 저는 연관관계를 &lt;b&gt;'누가 주인이냐'&lt;/b&gt;를 바탕으로 생각해보는 습관이있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이게 저에게는 뭔가 더 연관관계 매핑에 있어 편하게 다가왔습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상, notification이 주가 되고 notification_type이 종이 되는 것이 맞는 관계이므로,&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;그렇게 notification_type은 notification을 모르도록, 연관관계의 주인은 Notification이 되도록 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 에러코드 Enum값 다시 작성 시작하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xh547/dJMcaciv3Al/m4wGmFg9rhUgeS6lXFkSDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xh547/dJMcaciv3Al/m4wGmFg9rhUgeS6lXFkSDK/img.png&quot; data-alt=&quot;Error Code Enum&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xh547/dJMcaciv3Al/m4wGmFg9rhUgeS6lXFkSDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxh547%2FdJMcaciv3Al%2Fm4wGmFg9rhUgeS6lXFkSDK%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;800&quot; height=&quot;385&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Error Code Enum&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum으로 작성되는 ErrorCode를 생각해보면, Notification 도메인에만 존재하는 것이 아니라, 모든 도메인에 존재하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 추상화를 한단계 더 진행하여, &lt;b&gt;NotificationErrorCode가 Errorcode 구현체를 받도록 구성&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;1080&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOmj13/dJMcabRqz3d/tDrqCERTHmOLDPl7FN6JoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOmj13/dJMcabRqz3d/tDrqCERTHmOLDPl7FN6JoK/img.png&quot; data-alt=&quot;테스트 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOmj13/dJMcabRqz3d/tDrqCERTHmOLDPl7FN6JoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOmj13%2FdJMcabRqz3d%2FtDrqCERTHmOLDPl7FN6JoK%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;1080&quot; height=&quot;586&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 테스트를 작성하고나니 테스트가 모두 성공하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  결론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD를 처음 도입하여 작성해보며 느낀점이 굉장히 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD 자체가 주는 이점이 무엇인지를 다시 생각해보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD를 하게되면서 (제가 한 경험을 바탕으로 생각해보면) 도메인 코드를 순서 없이 냅다 와다다다 적는 것이 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 한 줄을 짜는데도 많은 생각을 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'연관 관계는 어떻게 설정하지? 양방향? 단방향?'부터 시작해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'여기서 필요한 코드가 뭘까? 이 에러가 발생한 원인을 해결하기 위해서는 어떤 코드를 짜면 좋을까?' 까지 코드를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짜는 과정에서 설계를 세분화하여 더 깊이 있게 생각하게 된다는 점이 제가 느낀 장점이었습니다.&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;p data-ke-size=&quot;size16&quot;&gt;팀원분과 이런저런 논의를 하다가 제가 고전파 테스트를 좋아한다는 점을 설명드렸던 적이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아마 한 일주일 전즈음 이었을 것입니다. 주간 스크럼을 진행하던 중 테스트에 모킹을 안좋아하기에 이에 대해 말씀드리다 논의하게 되었습니다.) 그러다 팀원분께서 아래의 글을 공유해주셔서 재밌게 읽어보아 같이 읽어보면 좋을 것 같아 공유드립니다. (샤라웃 의진님  )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.linkedin.com/posts/ethanseongilpark_github-atipico1ai-testing-rules-activity-7450767398204485632-8dz1/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.linkedin.com/posts/ethanseongilpark_github-atipico1ai-testing-rules-activity-7450767398204485632-8dz1/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776596427313&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;GitHub - Atipico1/ai-testing-rules | Ethan Park&quot; data-og-description=&quot;OpenAI에서는 테스트 코드를 어떻게 짜고 있을까? 클로드 코드, 코덱스 같은 에이전트를 쓰다 보면 자연스럽게 TDD 방식을 택하게 됩니다. 하지만 &amp;quot;어떻게 해야 좋은 테스트를 만들 수 있을까&amp;quot;를 고&quot; data-og-host=&quot;kr.linkedin.com&quot; data-og-source-url=&quot;https://www.linkedin.com/posts/ethanseongilpark_github-atipico1ai-testing-rules-activity-7450767398204485632-8dz1/&quot; data-og-url=&quot;https://kr.linkedin.com/posts/ethanseongilpark_github-atipico1ai-testing-rules-activity-7450767398204485632-8dz1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bj2xKD/dJMb8Z3tdAb/yFDkpI6XFkK8MpkOvjTKk0/img.png?width=1400&amp;amp;height=800&amp;amp;face=0_0_1400_800,https://scrap.kakaocdn.net/dn/JNqLB/dJMb9frHnoR/Se2PF5xzP8CP1S1rAldQ71/img.png?width=1400&amp;amp;height=800&amp;amp;face=0_0_1400_800,https://scrap.kakaocdn.net/dn/cEYsLc/dJMb9cBJ1Cs/MafNkQ3ysx8tkMGkq2sHL0/img.jpg?width=800&amp;amp;height=200&amp;amp;face=0_0_800_200&quot;&gt;&lt;a href=&quot;https://www.linkedin.com/posts/ethanseongilpark_github-atipico1ai-testing-rules-activity-7450767398204485632-8dz1/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.linkedin.com/posts/ethanseongilpark_github-atipico1ai-testing-rules-activity-7450767398204485632-8dz1/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bj2xKD/dJMb8Z3tdAb/yFDkpI6XFkK8MpkOvjTKk0/img.png?width=1400&amp;amp;height=800&amp;amp;face=0_0_1400_800,https://scrap.kakaocdn.net/dn/JNqLB/dJMb9frHnoR/Se2PF5xzP8CP1S1rAldQ71/img.png?width=1400&amp;amp;height=800&amp;amp;face=0_0_1400_800,https://scrap.kakaocdn.net/dn/cEYsLc/dJMb9cBJ1Cs/MafNkQ3ysx8tkMGkq2sHL0/img.jpg?width=800&amp;amp;height=200&amp;amp;face=0_0_800_200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Atipico1/ai-testing-rules | Ethan Park&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OpenAI에서는 테스트 코드를 어떻게 짜고 있을까? 클로드 코드, 코덱스 같은 에이전트를 쓰다 보면 자연스럽게 TDD 방식을 택하게 됩니다. 하지만 &quot;어떻게 해야 좋은 테스트를 만들 수 있을까&quot;를 고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kr.linkedin.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면, AI로 제가 좋아하는 고전파 테스트를 가능하게 해주는 프롬프트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 Test까지 AI로 자동화를 시킬 수 있다면, 작업할 때 더 큰 효율을 낼 수 있지 않을까? 에 대한 생각도 해보게 되었으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 도입을 고려해보게 될 것 같습니다 ☺️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/46</guid>
      <comments>https://xeulbn-dev.tistory.com/46#entry46comment</comments>
      <pubDate>Sun, 19 Apr 2026 20:05:51 +0900</pubDate>
    </item>
    <item>
      <title>(CS Study) Docker 컨테이너 부터 Kubernetes까지 (feat. namespace가 바꾼 세상)</title>
      <link>https://xeulbn-dev.tistory.com/45</link>
      <description>&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;작은 리눅스를 띄우는 거구나&lt;/span&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도커를 여러 개 관리해주는 도구구나&lt;/span&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;틀린 말은 아니지만 이정도의 이해로는 네트워크의 문제가 생겼을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 Pod간 통신이 안될 때, 정확한 원인을 짚어내기 어려울 것으로 생각되어집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓰게 된 이유는 단순합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 기술을 쓰면서도 &quot;왜 이렇게 동작하는가&quot;에 대한 명확한 답을 모르고 있었고,&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;그래서 이 글에서는 컨테이너의 본질인 리눅스 네임스페이스부터 시작해서 Docker가 그 위에서 무엇을 추상화했는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Kubernetes가 왜 필요해졌는지를 기술적으로 깊이있게 다뤄보려고 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 컨테이너의 본질 : Namespace&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서도 도커에 대해 포트포워딩을 공부하며 위 단어가 나온 것을 확인해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://xeulbn-dev.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://xeulbn-dev.tistory.com/34&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776336202724&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;(CS 스터디) 왜 Docker는 포트포워딩이 필요할까? - Linux Network Namespace부터 iptables까지&quot; data-og-description=&quot;  들어가기 전에어느 때처럼 평화롭게 일을 하고 있었습니다.Docker Hub 이미지를 기반으로 컨테이너를 띄우고, 포트 매핑을 설정하고, AWS 환경 위에 서비스를 올리며 배포 구성을 마무리해가던 &quot; data-og-host=&quot;xeulbn-dev.tistory.com&quot; data-og-source-url=&quot;https://xeulbn-dev.tistory.com/34&quot; data-og-url=&quot;https://xeulbn-dev.tistory.com/34&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cin05b/dJMb9lMc8lR/Jv9fsvZCeTF0O9BEc15DT1/img.png?width=738&amp;amp;height=394&amp;amp;face=0_0_738_394,https://scrap.kakaocdn.net/dn/bv53zw/dJMb9iaSLzU/V6cKfdiiVExlB79jIwnfk0/img.png?width=738&amp;amp;height=394&amp;amp;face=0_0_738_394,https://scrap.kakaocdn.net/dn/bKAr3f/dJMb8RRTuVI/3KmEKR33CKPYKmT6oW1pfk/img.png?width=863&amp;amp;height=549&amp;amp;face=0_0_863_549&quot;&gt;&lt;a href=&quot;https://xeulbn-dev.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://xeulbn-dev.tistory.com/34&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cin05b/dJMb9lMc8lR/Jv9fsvZCeTF0O9BEc15DT1/img.png?width=738&amp;amp;height=394&amp;amp;face=0_0_738_394,https://scrap.kakaocdn.net/dn/bv53zw/dJMb9iaSLzU/V6cKfdiiVExlB79jIwnfk0/img.png?width=738&amp;amp;height=394&amp;amp;face=0_0_738_394,https://scrap.kakaocdn.net/dn/bKAr3f/dJMb8RRTuVI/3KmEKR33CKPYKmT6oW1pfk/img.png?width=863&amp;amp;height=549&amp;amp;face=0_0_863_549');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;(CS 스터디) 왜 Docker는 포트포워딩이 필요할까? - Linux Network Namespace부터 iptables까지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전에어느 때처럼 평화롭게 일을 하고 있었습니다.Docker Hub 이미지를 기반으로 컨테이너를 띄우고, 포트 매핑을 설정하고, AWS 환경 위에 서비스를 올리며 배포 구성을 마무리해가던&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;xeulbn-dev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;네임스페이스 (Namespace)로 격리된 프로세스의 집합&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상머신처럼 별도의 커널을 띄우는 것이 아니라 &lt;b&gt;호스트 OS의 동일한 커널 위에서 실행&lt;/b&gt;되되,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리눅스 커널의 네임스페이스 기능을 통해 각 프로세스가 서로를 볼 수 없도록 격리된 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(PID namespace 관점 &amp;rarr; 아래에서 더 설명하도록 하겠습니다.)&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnpbim/dJMcahxl5bY/cJa7GJr4lOzcICzcy6RgBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnpbim/dJMcahxl5bY/cJa7GJr4lOzcICzcy6RgBk/img.png&quot; data-alt=&quot;현재 프로젝트 ec2위의 활성화된 namespace들 (feat. github-actions-runner)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnpbim/dJMcahxl5bY/cJa7GJr4lOzcICzcy6RgBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnpbim%2FdJMcahxl5bY%2FcJa7GJr4lOzcICzcy6RgBk%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;825&quot; height=&quot;170&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;170&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 프로젝트 ec2위의 활성화된 namespace들 (feat. github-actions-runner)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 lsns로 대표 프로세스 기준으로 묶어서 확인해본 결과 위와 같은 namespace들을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 개발 중인 프로젝트의 도커 컨테이너 위에서의 PID namespace를 확인해보면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tojgb/dJMcahc1883/bn3kLbpnKKVoJtrZE4inqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tojgb/dJMcahc1883/bn3kLbpnKKVoJtrZE4inqk/img.png&quot; data-alt=&quot;현재 Docker에서의 PID namespace&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tojgb/dJMcahc1883/bn3kLbpnKKVoJtrZE4inqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTojgb%2FdJMcahc1883%2Fbn3kLbpnKKVoJtrZE4inqk%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;641&quot; height=&quot;104&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 Docker에서의 PID namespace&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 호스트에서 실제 PID를 확인해보면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (17).png&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5GARO/dJMcadVX7IE/I3DJG7UaKYaMwVzABZhscK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5GARO/dJMcadVX7IE/I3DJG7UaKYaMwVzABZhscK/img.png&quot; data-alt=&quot;현재 Host에서의 동일한 컨테이너의 PID&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5GARO/dJMcadVX7IE/I3DJG7UaKYaMwVzABZhscK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5GARO%2FdJMcadVX7IE%2FI3DJG7UaKYaMwVzABZhscK%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;737&quot; height=&quot;36&quot; data-filename=&quot;image (17).png&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;36&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 Host에서의 동일한 컨테이너의 PID&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 namespace가 어떤 역할들을 하는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 Host에서의 PID namespace와 Docker에서의 PID namespace가 다른지 정리해보겠습니다.&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;PID namespace&lt;/b&gt; : &lt;br /&gt;컨테이너 내부에서는 자신의 PID 1번 프로세스(보통 init 또는 앱 프로세스)만 보입니다.&lt;br /&gt;호스트의 다른 프로세스는 보이지 않습니다.&lt;br /&gt;PID namespace는 프로세스의 ID를 namespace별로 별도로 매핑하여 동일한 프로세스가 다른 PID로 보이게 합니다. &lt;br /&gt;또한 커널은 같은 namespace에 속한 프로세스만 조회하도록 제한하기 때문에 프로세스 격리가 이루어지게 되는 것입니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NET namespace&lt;/b&gt; : &lt;br /&gt;독립된 네트워크 스택(인터페이스, IP, 포트, 라우팅 테이블)을 갖게 됩니다.&lt;br /&gt;(아래 2. 컨테이너마다 IP가 다른 이유에서 자세히 설명하겠습니다.)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MNT namespace&lt;/b&gt; : &lt;br /&gt;독립된 파일시스템 마운트 포인트를 갖습니다.&lt;br /&gt;여기서 /bin, /lib 등이 컨테이너 전용으로 보이는 이유가 설명됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UTS namespace&lt;/b&gt; : &lt;br /&gt;독립된 hostname을 설정할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IPC namespace&lt;/b&gt; : &lt;br /&gt;독립된 프로세스간 통신 (공유 메모리, 세마포어) 공간을 갖게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;User namespace&lt;/b&gt; : &lt;br /&gt;컨테이너 내부의 root가 호스트의 일반 사용자로 매핑될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네임스페이스가 격리를 담당한다면, cgroup (Control Group)은 자원 제한을 담당하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker의 CPU/메모리 제한은 cgroup 파일 시스템(/sys/fs/cgroup)의 cpu.max, memory.max 값을 통해 실제로 적용되며,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776434004660&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cgroup&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;을 통해 해당 프로세스가 어떤 cgroup에 속해있는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 cgroup 문서를 들어가 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (18).png&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHiIzS/dJMcagZtTbB/pqysKoxSx3MveMqAMZX26K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHiIzS/dJMcagZtTbB/pqysKoxSx3MveMqAMZX26K/img.png&quot; data-alt=&quot;현재 진행 중인 프로젝트의 cgroup 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHiIzS/dJMcagZtTbB/pqysKoxSx3MveMqAMZX26K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHiIzS%2FdJMcagZtTbB%2FpqysKoxSx3MveMqAMZX26K%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;1092&quot; height=&quot;217&quot; data-filename=&quot;image (18).png&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 진행 중인 프로젝트의 cgroup 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 cpu, memory에 특별한 제한이 걸려있지는 않은 점을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 지금까지 CPU는 약 169초 사용했으며 유저 영역에서 사용한 CPU 시간은 약 107초,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널 영역에서 사용한 CPU 시간은 약 61초로 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;usage_usec = user_usec + system_usec 가 맞아떨어지는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;또한, nr_periods(CPU quota 체크 횟수) 와 nr_throttled(CPU 제한 때문에 막힌 횟수)와 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;nr_burst / burst_usec (CPU burst허용 관련 기능이라고 합니다. 이번에 처음 알게 되었습니다..)까지 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;현재 cpu나 memory에 특별한 제약이 없기에, nr_periods / nr_throttled값은 0인 것을 확인할 수 있습니다.&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;이렇듯 특정 컨테이너가 CPU를 50% 이상 사용하지 못하게 하거나, 메모리 상한을 설정하는 것이 cgroup이 하는 일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 서로 자원을 뺏지 못하도록 보장하는 메커니즘입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(추가) 그렇다면 왜 지금 CPU나 memory 사용량에 제한을 두지 않았나요?&lt;/p&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;현재 제한적인 자원의 환경(aws 낮은 티어)에서는 CPU와 메모리가 제한되어 있기 때문에 여러 컨테이너간의 경합이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 특정 컨테이너가 독점을 하게 된다면 다른 서비스의 성능 저하를 유발할 수 있기에 반드시 리소스 제한이 필요한 것도 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다만&lt;/b&gt; 현재 트래픽이 제한적인 dev환경에서 운영되고 있기 때문에, 각 컨테이너의 실제 리소스 사용 패턴을 먼저 파악하는 것이 중요하다고 판단했습니다. 초기부터 CPU나 메모리에 제한을 두게 되면 병목의 원인이 왜곡될 수 있기 때문에, 우선은 제한 없이 운영하면서 cpu.stat과 memory 사용량을 통해 baseline을 수집하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 실제 사용량을 기반으로 적절한 CPU 및 메모리 제한을 설정할 계획입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서 결론적으로 컨테이너가 OS처럼 보이는 이유는 독립된 루트 파일시스템(rootfs)을 갖고 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 rootfs가 Ubuntu든 Alpine이든 상관없이 그 안에 /bin, /lib같은 디렉토리가 있기에 리눅스처럼 보이게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;실행되는 커널은 단 하나, 호스트의 커널입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진을 통해 같은 커널이라는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (20).png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;123&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blkpL2/dJMcabcNeuM/bkkUsgKxbA26MlYScF1du0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blkpL2/dJMcabcNeuM/bkkUsgKxbA26MlYScF1du0/img.png&quot; data-alt=&quot;실제 Docker 컨테이너와 호스트의 uname 정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blkpL2/dJMcabcNeuM/bkkUsgKxbA26MlYScF1du0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblkpL2%2FdJMcabcNeuM%2FbkkUsgKxbA26MlYScF1du0%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;1420&quot; height=&quot;123&quot; data-filename=&quot;image (20).png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;123&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실제 Docker 컨테이너와 호스트의 uname 정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uname 명령어는 현재 시스템의 커널 정보를 출력하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 내부와 호스트에서 동일한 결과가 나온다면 동일한 커널을 공유하고 있다는 것을 의미합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 네트워크 격리 : 컨테이너마다 IP가 다른 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 쓰다보면, docker inspect &amp;lt;container&amp;gt;로 IP를 확인해보면, 아래와 같이 나오게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(현재 너무 길어서, IP주소만 떼서 볼 수 있도록 아래의 명령어를 사용하였습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (19).png&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDMGd/dJMcadhoZIr/hRCH0nORPTSNIZfRkLDGR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDMGd/dJMcadhoZIr/hRCH0nORPTSNIZfRkLDGR1/img.png&quot; data-alt=&quot;Docker 컨테이너별 IP&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDMGd/dJMcadhoZIr/hRCH0nORPTSNIZfRkLDGR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDMGd%2FdJMcadhoZIr%2FhRCH0nORPTSNIZfRkLDGR1%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;1115&quot; height=&quot;142&quot; data-filename=&quot;image (19).png&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker 컨테이너별 IP&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 서버인데, 같은 ec2인데 IP 주소가 다르게 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 서버인데도 이렇게 다른 IP가 나오게 되는 이유가 바로 NET namespace 때문입니다.&lt;/p&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;독립된 가상 네트워크 인터페이스 (eth0)&lt;/li&gt;
&lt;li&gt;독립된 IP 주소&lt;/li&gt;
&lt;li&gt;독립된 포트 공간&lt;br /&gt;(컨테이너 A와 B가 둘 다 8080포트를 열 수 있는 이유&amp;nbsp;&amp;rarr; 이전 포스트 참조)&lt;/li&gt;
&lt;li&gt;독립된 라우팅 테이블&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가볍게 정리하고 넘어가면, &lt;b&gt;Docker는 기본적으로 docker0라는 가상 브리지 네트워크를 호스트에 생성&lt;/b&gt;하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 컨테이너의 eth0를 veth(virtual ethernet) 페어를 통해 이 브리지에 연결&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨테이너 간 통신은 이 브리지를 통해&lt;/b&gt; 이루어지고, &lt;b&gt;외부 통신은 호스트의 NAT(iptables)를 통해&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;642&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nwiOQ/dJMcagd7VqK/qfQNHOux4XUGcFModB42qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nwiOQ/dJMcagd7VqK/qfQNHOux4XUGcFModB42qK/img.png&quot; data-alt=&quot;Docker의 Linux Host OS 위에서의 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nwiOQ/dJMcagd7VqK/qfQNHOux4XUGcFModB42qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnwiOQ%2FdJMcagd7VqK%2FqfQNHOux4XUGcFModB42qK%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;642&quot; height=&quot;383&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker의 Linux Host OS 위에서의 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 컨테이너 A에서 컨테이너 B로 패킷을 보내면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷은 veth0 &amp;rarr; docker0 브리지 &amp;rarr; veth1 경로로 흐르게 됩니다.&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;-p 8080:80 옵션으로 포트를 퍼블리시할 때는 iptables에 DNAT(Destination NAT) 규칙이 추가되어,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트의 8080으로 들어오는 패킷이 컨테이너의 80포트로 포워딩됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 또한 커널 수준의 네트워크 기능을 Docker가 자동으로 설정해주는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 또한 실제로 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776435934335&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo iptables -t nat -L -n -v&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;위 명령어를 입력하게 되면 리눅스 커널의 NAT (주소 변환) 규칙을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 디자인 (6).png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yLhIt/dJMcaibYjqV/4xm6YnYzcrokOJ19M9IAtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yLhIt/dJMcaibYjqV/4xm6YnYzcrokOJ19M9IAtK/img.png&quot; data-alt=&quot;현재 예비 ec2 리눅스 커널의 NAT 규칙&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yLhIt/dJMcaibYjqV/4xm6YnYzcrokOJ19M9IAtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyLhIt%2FdJMcaibYjqV%2F4xm6YnYzcrokOJ19M9IAtK%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;1228&quot; height=&quot;528&quot; data-filename=&quot;제목 없는 디자인 (6).png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 예비 ec2 리눅스 커널의 NAT 규칙&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 iptables의 NAT 테이블을 확인해보면 Docker가 자동으로 Docker 체인을 생성하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 체인에 DNAT 규칙을 추가하여 포트포워딩을 수행하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 POSTROUTING 체인에서는 MASQUERADE 규칙(출발지 IP를 자동으로 호스트 IP로 바꿔주는 출발지 IP를 바꿔주는 기술입니다.)&lt;br /&gt;이 적용되어 컨테이너에서 외부로 나가는 트래픽의 출발지 IP를 호스트 IP로 변환하는 것도 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방법으로 Docker의 네트워크 기능은 별도의 네트워크 스택이 아니라&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리눅스 커널의 iptables 기반 NAT 기능을 활용한 것임&lt;/b&gt;을 직접 확인해볼 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Docker : 위에서 말한 복잡한 리눅스 기능을 사람이 편하게 쓸 수 있게 만든 도구&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지의 내용은 사실 리눅스 커널이 원래부터 제공해주던 기능들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Namespace는 2002년부터, cgroup은 2007년부터 리눅스 커널에 존재했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 등장하기 이전에도 LXC(Linux Containers)같은 도구가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker의 혁신은 이 복잡한 커널 기능들을 개발자가 쉽게 쓸 수 있는 워크플로우로 포장해 제공한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 해결한 문제들을 하나씩 보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1) 이미지 기반 배포&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 실행하기 위한 rootfs와 메타데이터 (실행 명령, 환경 변수, 포트 설정 등)를&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;Union File System을 활용해서 이미지 레이어를 공유하고, 컨테이너 실행 시에는 쓰기 가능한 레이어만 추가합니다.&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;size18&quot;&gt;&lt;b&gt;2) 환경 일관성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 안에 앱 실행에 필요한 라이브러리, 설정, 바이너리를 모두 포함시키기 때문에 실행 환경이 동일하게 유지됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3) 실행 간편화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776353346782&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -p 8080:80 nginx&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;와 같이 명령어 한 줄이면 nginx 컨테이너가 뜨게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 이미지 pull, 네임스페이스 생성, cgroup 설정, 네트워크 연결, rootfs 마운트가 모두 자동으로 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, Docker는 &lt;b&gt;본질적으로 단일 호스트에서 컨테이너를 실행하는 도구&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 아래와 같은 의문이 따라오게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컨테이너가 수십, 수백 개로 늘어나면 어떻게 될까요?&lt;/span&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;트래픽이 몰렸을 때 컨테이너를 자동으로 늘려주지도 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서버에 걸쳐 컨테이너를 배포하고 서비스를 연결하는 것은 결국 수동으로 해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Docker는 컨테이너 실행에 목적이 있지만, 대규모로 운영하는 것은 전혀 다른 문제가됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Kubernetes : 컨테이너 운영 시스템의 등장&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes(K8s)는 Google이 내부적으로 수년간 컨테이너 오케스트레이션을 위해 사용하던 Borg 시스템의 경험을 바탕으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2014년 오픈소스로 공개한 프로젝트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트의 목적은 컨테이너를 대규모로 자동 관리하는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(여기서 잠깐) 쿠버네티스에서 쓰이는 용어를 알고 가겠습니다!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스를 우선 쉽게 설명을 드리면, 다수의 컨테이너를 효율적으로 배포, 확장 및 관리하기 위한 오픈소스 시스템으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose의 확장판이라고 생각해주시면 됩니다!&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;830&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blMDBT/dJMcaiJJZ97/tUaksX0wZXkJepkiP1coJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blMDBT/dJMcaiJJZ97/tUaksX0wZXkJepkiP1coJ0/img.png&quot; data-alt=&quot;쿠버네티스의 일반적인(?) 구성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blMDBT/dJMcaiJJZ97/tUaksX0wZXkJepkiP1coJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblMDBT%2FdJMcaiJJZ97%2FtUaksX0wZXkJepkiP1coJ0%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;2070&quot; height=&quot;830&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;830&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠버네티스의 일반적인(?) 구성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 Pod (파드) 라는 용어가 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod : Kubernetes에서 배포되는 가장 작은 실행 단위 라고 생각해주시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 볼 수 있듯이, 단일 컨테이너 Pod이 있을 수 있고, 예외적으로 멀티 컨테이너 Pod이 있을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DiYfU/dJMcafM5MJo/tEUwK4QtNg4ELK8zMtlOTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DiYfU/dJMcafM5MJo/tEUwK4QtNg4ELK8zMtlOTk/img.png&quot; data-alt=&quot;쿠버네티스의 DEPLOYMENT&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DiYfU/dJMcafM5MJo/tEUwK4QtNg4ELK8zMtlOTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDiYfU%2FdJMcafM5MJo%2FtEUwK4QtNg4ELK8zMtlOTk%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;1060&quot; height=&quot;650&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠버네티스의 DEPLOYMENT&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deployment (디플로이먼트)와 ReplicaSet (레플리카셋) 이라는 용어도 볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, ReplicaSet이란, 복제본끼리의 묶음을 레플리카 셋이라고 일컫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deployment는 원하는 개수와 상태로 유지해주는 관리 객체입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 정확히 말하자면, Pod를 생성, 삭제, 업데이트(롤링배포)까지 관리하는 컨트롤러입니다.&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;836&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcPcOX/dJMcaiQvQjF/AMBlIa3Dqm3AdWbaxkJkS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcPcOX/dJMcaiQvQjF/AMBlIa3Dqm3AdWbaxkJkS0/img.png&quot; data-alt=&quot;쿠버네티스의 Service&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcPcOX/dJMcaiQvQjF/AMBlIa3Dqm3AdWbaxkJkS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcPcOX%2FdJMcaiQvQjF%2FAMBlIa3Dqm3AdWbaxkJkS0%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;836&quot; height=&quot;770&quot; data-origin-width=&quot;836&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿠버네티스의 Service&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사진에서는 또 서비스라는 단어가 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes에서의 Service란 외부로부터 들어오는 트래픽을 받아서 Pod(파드)에 균등하게 분배해주는 로드밸런서 역할을 하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자로부터 요청을 받는 역할을 하는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실제 서비스에서 Pod에 요청을 보낼 때, 포트포워딩이나 Pod내로 직접 접근해 요청을 보내지는 않습니다. 서비스를 통해 보내는게 일반적인데, 이 이유는 아래에서 자세히 설명하도록 하겠습니다.☺️)&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 돌아와서 Kubernetes가 해결하는 핵심 문제들은 아래와 같습니다.&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;자가 치유 (Self-healing)&lt;/b&gt; : 컨테이너 (정확히는 Pod에 가까울 것 같습니다.)가 죽으면 자동으로 재시작하거나 다른 노드에 재배치합니다. 개발자가 새벽에 알람 받을 일이 줄어듭니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 스케일링&lt;/b&gt; : CPU나 메모리 사용률에 따라 Pod 수를 자동으로 늘리고 줄입니다.&lt;br /&gt;(HPA, Horizontal Pod Autoscaler), 트래픽 급증에 자동으로 대응합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선언적 배포&lt;/b&gt; : '이 서비스는 컨테이너 3개가 항상 실행되어야 한다.' 고 YAML로 선언하게 되면,&amp;nbsp;&lt;br /&gt;Kubernetes가 현재 상태를 원하는 상태로 계속 맞춰갑니다.&lt;br /&gt;개발자는 어떤 상태를 만들지만 정의하면 Kubernetes에서 내부적으로 해결해주는 것입니다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;롤링 업데이트와 롤백&lt;/b&gt; : 새 버전의 이미지를 배포할 때 서비스 중단 없이 점진적으로 교체합니다.&lt;br /&gt;문제가 생기면 이전 버전으로 즉시 롤백하게 됩니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 디스커버리와 로드 밸런싱&lt;/b&gt; : 컨테이너 IP는 재시작할 때마다 바뀌게 됩니다.&lt;br /&gt;Kubernetes의 Service 오브젝트가 고정된 DNS이름과 가상 IP(Cluster IP)를 제공하고,&lt;br /&gt;트래픽을 뒤에 있는 Pod들에 분산시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Docker와 Kubernetes의 계층 관계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker의 역할은 컨테이너 이미지를 만들고 (docker build), 단일 호스트에서 컨테이너를 실행 (docker run)하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes의 역할은 여러 호스트에 걸쳐 컨테이너를 배치, 관리, 복구, 확장하는 것입니다.&lt;/p&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eT0Ir8/dJMcacbJgNB/8z3aHQhecVLTkCab5VLkeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eT0Ir8/dJMcacbJgNB/8z3aHQhecVLTkCab5VLkeK/img.png&quot; data-alt=&quot;Kubernetes 동작 계층 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eT0Ir8/dJMcacbJgNB/8z3aHQhecVLTkCab5VLkeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeT0Ir8%2FdJMcacbJgNB%2F8z3aHQhecVLTkCab5VLkeK%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;385&quot; height=&quot;642&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kubernetes 동작 계층 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 컨테이너를 직접 실행하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRI (Container Runtime Interface)를 통해 컨테이너 런타임에 위임합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-17 오후 11.51.55.png&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lgXm9/dJMcajaOGd8/KM1uEAeVyklgCHPwoOmeBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lgXm9/dJMcajaOGd8/KM1uEAeVyklgCHPwoOmeBK/img.png&quot; data-alt=&quot;Kubernetes 공식문서 (https://kubernetes.io/docs/concepts/containers/cri/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lgXm9/dJMcajaOGd8/KM1uEAeVyklgCHPwoOmeBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlgXm9%2FdJMcajaOGd8%2FKM1uEAeVyklgCHPwoOmeBK%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;989&quot; height=&quot;284&quot; data-filename=&quot;스크린샷 2026-04-17 오후 11.51.55.png&quot; data-origin-width=&quot;989&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kubernetes 공식문서 (https://kubernetes.io/docs/concepts/containers/cri/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 Docker가 기본 런타임이었으나, 지금은 containered나 CRI-O(둘 다 Kubernetes가 컨테이너 실행을 위임하는 런타임입니다!)가 주로 사용됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ev60R/dJMcaiiHMKC/qm2kTV5YL2oBXqYKNsgz8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ev60R/dJMcaiiHMKC/qm2kTV5YL2oBXqYKNsgz8k/img.png&quot; data-alt=&quot;Dockershim이 기본 runtime에서 제거되었다는 공식문서 (https://kubernetes.io/docs/setup/production-environment/container-runtimes/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ev60R/dJMcaiiHMKC/qm2kTV5YL2oBXqYKNsgz8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEv60R%2FdJMcaiiHMKC%2Fqm2kTV5YL2oBXqYKNsgz8k%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;619&quot; height=&quot;169&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Dockershim이 기본 runtime에서 제거되었다는 공식문서 (https://kubernetes.io/docs/setup/production-environment/container-runtimes/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 이것과 관련된 재미있는 블로그 글이 있어 가져와보았습니다 ㅎㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgrqbP/dJMb9969bLf/oDAHnVMwVUzzwy7Tj6mnYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgrqbP/dJMb9969bLf/oDAHnVMwVUzzwy7Tj6mnYk/img.png&quot; data-alt=&quot;Docker가 기본 런타임이 아니라는 글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgrqbP/dJMb9969bLf/oDAHnVMwVUzzwy7Tj6mnYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgrqbP%2FdJMb9969bLf%2FoDAHnVMwVUzzwy7Tj6mnYk%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;1001&quot; height=&quot;299&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker가 기본 런타임이 아니라는 글&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Panic에 걸리지 말라며....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 Docker로부터 deprecating 되었다는 글을 아래 주소에서 확인해보실 수 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776437727497&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Don't Panic: Kubernetes and Docker&quot; data-og-description=&quot;Update: Kubernetes support for Docker via dockershim is now removed. For more information, read the removal FAQ. You can also discuss the deprecation via a dedicated GitHub issue. Kubernetes is deprecating Docker as a container runtime after v1.20. You do &quot; data-og-host=&quot;kubernetes.io&quot; data-og-source-url=&quot;https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/&quot; data-og-url=&quot;https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Don't Panic: Kubernetes and Docker&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Update: Kubernetes support for Docker via dockershim is now removed. For more information, read the removal FAQ. You can also discuss the deprecation via a dedicated GitHub issue. Kubernetes is deprecating Docker as a container runtime after v1.20. You do&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kubernetes.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker로 빌드한 이미지는 결국 OCI(Open Container Initiative) 표준을 따르기 때문에 containered에서도 그대로 실행됩니다.&lt;/p&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;중요한 포인트는 Kubernetes도 결국 리눅스 네임스페이스와 cgroup 위에서 동작한다는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추상화 수준이 높아졌을 뿐, 컨테이너의 본질은 변하지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. Kubernetes 네트워크&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker에서 배운 NET namespace 개념이 Kubernetes에서 어떤 식으로 확장되는지 보면 전체 구조가 훨씬 명확해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes의 최소 배포 단위는 Pod입니다. (위 용어 설명 참고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod는 하나 &lt;b&gt;이상의&lt;/b&gt;(하나 일수도 있고, 아닐 수도 있습니다!) 컨테이너 묶음인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 특성은 이 Pod 내부 컨테이너들이 동일한 NET namespace를 공유한다는 점입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3ubNH/dJMcaayesmP/yGYfIxvbgHcEUBrLjRekK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3ubNH/dJMcaayesmP/yGYfIxvbgHcEUBrLjRekK1/img.png&quot; data-alt=&quot;Kubernetes Pod의 내부 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3ubNH/dJMcaayesmP/yGYfIxvbgHcEUBrLjRekK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3ubNH%2FdJMcaayesmP%2FyGYfIxvbgHcEUBrLjRekK1%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;966&quot; height=&quot;496&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kubernetes Pod의 내부 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 A와 B는 같은 IP를 갖고, 서로 localhost로 통신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 가능한 이유가 바로 &lt;b&gt;같은 NET namespace를 공유&lt;/b&gt;하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Kubernetes는 &lt;b&gt;pause라는 인프라 컨테이너를 먼저 시작해서 Pod의 NET namespace를 생성&lt;/b&gt;하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;나머지 컨테이너들이 이 namespace를 공유하는 방식으로 동작&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 Pod간 통신은 어떻게 될까요?&lt;br /&gt;&lt;/span&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;Kubernetes의 네트워크 모델은 아래 원칙을 따릅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GDmOe/dJMcagrENS4/YkNcjV7l3Kv3YfYPoDXlsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GDmOe/dJMcagrENS4/YkNcjV7l3Kv3YfYPoDXlsk/img.png&quot; data-alt=&quot;Kubernetes의 Cluster Network&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GDmOe/dJMcagrENS4/YkNcjV7l3Kv3YfYPoDXlsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGDmOe%2FdJMcagrENS4%2FYkNcjV7l3Kv3YfYPoDXlsk%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;1147&quot; height=&quot;685&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;685&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kubernetes의 Cluster Network&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 Pod는 고유한 IP를 갖습니다.&lt;/li&gt;
&lt;li&gt;모든 Pod는 NAT 없이 다른 모든 Pod와 직접 통신할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;Pod가 바라보는 자신의 IP와 다른 Pod가 바라보는 그 Pod의 IP는 동일해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 원칙을 구현하는 것은 CNI (Container Network Interface) 플러그인이 담당하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JM7gJ/dJMcahxl6Mx/qbrEVgjLBMIMRCJ1giI41k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JM7gJ/dJMcahxl6Mx/qbrEVgjLBMIMRCJ1giI41k/img.png&quot; data-alt=&quot;CNI Plugin Layer 설명&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JM7gJ/dJMcahxl6Mx/qbrEVgjLBMIMRCJ1giI41k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJM7gJ%2FdJMcahxl6Mx%2FqbrEVgjLBMIMRCJ1giI41k%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;1136&quot; height=&quot;245&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;CNI Plugin Layer 설명&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Calico, Flannel, Cilium 등이 대표적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CNI 플러그인은&lt;b&gt; 각 노드에 오버레이 네트워크(물리 네트워크 위 가짜 네트워크)나 라우팅 룰을 설정&lt;/b&gt;해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 노드에 있는 Pod들이 마치 같은 네트워크에 있는 것처럼 통신할 수 있게 만듭니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbIHzw/dJMcaiXgXpp/mgEtSucGFskA0466JGF3P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbIHzw/dJMcaiXgXpp/mgEtSucGFskA0466JGF3P0/img.png&quot; data-alt=&quot;Overlay Network를 통한 실제 통신 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbIHzw/dJMcaiXgXpp/mgEtSucGFskA0466JGF3P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbIHzw%2FdJMcaiXgXpp%2FmgEtSucGFskA0466JGF3P0%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;1002&quot; height=&quot;212&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Overlay Network를 통한 실제 통신 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그러나&lt;/b&gt; Pod이 삭제되고 새 Pod로 재생되거나 다른 노드로 다시 스케줄링되면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Pod IP는&lt;span&gt; &lt;/span&gt;&lt;/span&gt;바뀌게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 문제를 해결하려면 어떻게 할 수 있을까요?&lt;/span&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;b&gt;고정된 IP를 제공하면 될 것&lt;/b&gt;이라는 생각이듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞습니다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kubernetes의 Service 오브젝트(위 용어 설명 참조!)는 ClusterIP (가상 IP)와 DNS 이름을 제공하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kube-proxy(또는 Cilium같은 다른 서비스 프록시 구현체)가 Service와 Endpoint 정보를 감시하면서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;iptables, nftables 규칙을 동적으로 관리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트래픽을 직접 중계하는 것이 아니라, 커널이 규칙에 따라 패킷을 뒤에 있는 Pod로 전달하는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-18 오전 12.24.34.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKZSXX/dJMcafM5Nr9/e8aRHIqYWf6Ll1JY84kIr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKZSXX/dJMcafM5Nr9/e8aRHIqYWf6Ll1JY84kIr0/img.png&quot; data-alt=&quot;Kubernetes의 nftables 권장 (https://kubernetes.io/docs/reference/networking/virtual-ips)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKZSXX/dJMcafM5Nr9/e8aRHIqYWf6Ll1JY84kIr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKZSXX%2FdJMcafM5Nr9%2Fe8aRHIqYWf6Ll1JY84kIr0%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;711&quot; height=&quot;341&quot; data-filename=&quot;스크린샷 2026-04-18 오전 12.24.34.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kubernetes의 nftables 권장 (https://kubernetes.io/docs/reference/networking/virtual-ips)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한때 많이 쓰이던 IPVS모드는 Kubernetes 1.33에서 deprecated 됐고, 현재는 nftables 방향으로 전환중으로 보입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;주황색 노란색 하늘색 행복한 히피 확언 동기부여적인 휴대폰 배경화면.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btZbW7/dJMcabcNfbo/y3tv0VmaVf4QJKvEBHTEOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btZbW7/dJMcabcNfbo/y3tv0VmaVf4QJKvEBHTEOk/img.png&quot; data-alt=&quot;Service 도입 후의 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btZbW7/dJMcabcNfbo/y3tv0VmaVf4QJKvEBHTEOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtZbW7%2FdJMcabcNfbo%2Fy3tv0VmaVf4QJKvEBHTEOk%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;1080&quot; height=&quot;1920&quot; data-filename=&quot;주황색 노란색 하늘색 행복한 히피 확언 동기부여적인 휴대폰 배경화면.png&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Service 도입 후의 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 Docker의 포트매핑처럼 고정된 앞단 주소를 제공하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤에 연결된 Pod 집합이 동적으로 바뀌더라도 같은 이름과 IP로 접근할 수 있다는 점에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훨씬 동적이고 추상화된 방식입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7.&amp;nbsp; 그렇다면 여러 서비스를 컨테이너화할 때 어떤 문제가 벌어질 수 있을까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 학습한 내용을 토대로 생각해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 API 서버, Redis 캐시를 각각 별도의 Pod로 배포하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드카(ex.로그수집기)를 같은 Pod로 배치한다고 생각해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x7VKX/dJMcaakF9cr/vnjH4LbyLwcPhQ3EtIbkhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x7VKX/dJMcaakF9cr/vnjH4LbyLwcPhQ3EtIbkhk/img.png&quot; data-alt=&quot;Kubernetes Cluster&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x7VKX/dJMcaakF9cr/vnjH4LbyLwcPhQ3EtIbkhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx7VKX%2FdJMcaakF9cr%2FvnjH4LbyLwcPhQ3EtIbkhk%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;1166&quot; height=&quot;602&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Kubernetes Cluster&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버 Pod에서 Redis에 접근할 때, redis-service:6379라는 DNS이름을 쓰게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 DNS는 Kubernetes의 CoreDNS가 redis-service라는 Service 오브젝트의 ClusterIP로 해석해주고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kube-proxy 또는 네트워크 구현체가 iptables, nftables 등의 방식으로 트래픽을 실제 Redis Pod로 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 유저&amp;nbsp;레벨&amp;nbsp;프록시를&amp;nbsp;거치지&amp;nbsp;않고&amp;nbsp;커널&amp;nbsp;데이터패스를&amp;nbsp;통해&amp;nbsp;처리되기&amp;nbsp;때문에&amp;nbsp;비교적&amp;nbsp;효율적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 API서버와 사이드카(ex. 로그 수집기)를 같은 Pod로 배치한다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Pod 내부의 컨테이너들은 동일한 NET namespace를 공유하므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;localhost로 통신할 수 있고 네트워크 홉이 발생하지 않습니다.&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;이런 패턴(사이드카 패턴)은 서비스 메시(Istio 등)에서 Envoy 프록시를 모든 Pod에서 사이드카로 붙이는 방식으로도 활용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 설계시에는 그렇기에 아래와 같은 trade-off를 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod간 통신은 CNI 구현 방식(overlay, routing 등)에 따라 레이턴시가 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고성능이 필요한 경우 동일 Pod 또는 동일 노드에 배치(Pod Affinity)를 고려할 수 있지만&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;반대로 장애 격리가 중요한 경우에는 Pod Anti-Affinity로 서로 다른 노드에 분산 배치하는 것이 바람직합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 처음 공부할 때는 그냥 작은 VM이다 생각하고 Docker를 사용해왔었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허나, 네트워크 문제가 생겼을 때, 성능 병목이 어디서 오는지를 파악해야할 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 namespace와 cgroup이라는 실제 메커니즘을 알고 있는 것과 모르는 것의 차이는 크다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 그 복잡한 커널 기능을 개발자가 쉽게 쓸 수 있게 추상화했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 그 위에서 대규모 운영에 필요한 자동화 레이어를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 스택의 가장 아래에는 항상 리눅스 커널의 namespace와 cgroup이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓰면서 다시 한 번 느낀건, 좋은 추상화는 밑에 있는 레이어를 숨기지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생한다면 결국 그 밑으로 내려가야만 한다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes가 아무리 복잡해 보여도, 결국 리눅스 프로세스 격리의 원리 위에 있습니다.&lt;/p&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;결국 서비스 메시(IStio), eBPF 기반 네트워킹 (Cilium), 그리고 컨테이너 런타임 보안(seccomp, AppArmor)도&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;AI가 발전하면서 새로운 기술에 대한 장벽도 많이 낮아졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이럴 때일수록 더 기본기로 돌아가야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 새로운 기술이 나왔을 때 새로운 기술을 적용해보고 학습해보는 것도 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 모든 것은 결국 기본에서 오기 때문에 AI를 더 현명하게 사용하기 위해서라도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본을 항상 놓치지말고 복습하고 열심히 깨우쳐 나가는 것이 중요한 것 같습니다.&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/45</guid>
      <comments>https://xeulbn-dev.tistory.com/45#entry45comment</comments>
      <pubDate>Sat, 18 Apr 2026 00:43:50 +0900</pubDate>
    </item>
    <item>
      <title>(CS Study) Redis를 메시지 브로커로 : Pub/Sub 가이드</title>
      <link>https://xeulbn-dev.tistory.com/44</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 여태 한 번도 사용해본 적이 없는 형태, Pub/Sub에 대해 이해해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 '채팅 기능 구현시 Redis의 Pub/Sub을 사용하면 고부하 환경에서도 실시간성을 보장할 수 있다. '라고만 들어왔지,&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;그렇기에 이 글에서는 Redis Pub/Sub에 집중하여 이해해보는 시간을 갖도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 선택의 폭과 판단력을 기르기 위해 기술적 선택지를 넓혀가는 시간을 가져보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Pub/Sub이란 무엇인가 : Redis에서의 위치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub은 Publisher(발행자)와 Subscriber(구독자)의 약자로 발행, 구독 패턴을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행자가 특정 채널에 메시지를 발행하면, 해당 채널을 구독하고 있는 모든 구독자가 실시간으로 그 메시지를 수신하는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴의 가장 큰 강점은 느슨한 결합(Loose Coupling)일 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 A는 서비스 B,C,D가 자신의 메시지를 구독하고 있다는 사실을 알 필요가 없습니닫.&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;Pub/Sub에는 두 가지 분리 특성이 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간적 분리 : 일반적인 Pub/Sub에서는 발행자와 구독자가 동시에 존재할 필요가 없습니다.&lt;br /&gt;하지만 Redis Pub/Sub은 중간에 메시지를 저장하지 않습니다. &lt;br /&gt;메시지의 영속성이 없기 때문에 완전하게 활용하려면 발행자와 구독자 모두 동시에 연결되어 있어야 합니다.&lt;/li&gt;
&lt;li&gt;공간적 분리 : 발행자는 구독자의 위치를 모르고 구독자는 발행자의 위치를 모릅니다.&lt;br /&gt;이 특성은 모든 메시지큐 시스템에서 공유하는 특징일 것 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Redis생태계 안에서 Pub/Sub은 어디에 위치할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub은 가장 끝단에 있는 가장 단순한 형태의 메시징 시스템입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소위 Fire &amp;amp; Forget 방식으로 메모리만 사용하는 초경량구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스펙트럼으로 놓고 보면 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776225543292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    Redis Pub/Sub     &amp;rarr;    Redis Streams     &amp;rarr;         RabbitMQ/Kafka
비영속 브로드캐스트 시스템      로그 기반 메시지 저장       영속성과 소비 보장을 제공하는 메시징 시스템&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;Redis Pub/Sub은 가볍고 유실이 허용되는 이벤트에만 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 전달되어야하는 중요한 메시지라면 Redis Pub/Sub의 대상이 아닌 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 핵심 명령어 - SUBSCRIBE, PSUBSCRIBE, PUBLISH&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub에는 세가지 핵심 명령어가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;일반 구독 - SUBSCRIBE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자는 하나 이상의 채널을 구독할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUBSCRIBE 명령을 실행하는 순간, 클라이언트는 구독 모드로 전환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 모드에서는 Pub/Sub 관련 명령 외에는 다른 Redis 명령을 실행할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 채널을 동시에 구독하는 것도 가능하며 각 채널로부터 독립적으로 메시지를 수신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳고 해제는 UNSUBSCRIBE 명령어로 처리됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776223412352&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;SUBSCRIBE news sports weather
UNSUBSCRIBE news&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;패턴구독 - PSUBSCRIBE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와일드카드 패턴으로 여러 채널을 한 번에 구독하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와일드카드에는 *과 ? 두가지를 사용할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;* : 0개 이상의 모든 문자를 의미&lt;/li&gt;
&lt;li&gt;? : 정확히 1개의 임의 문자를 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1776223412352&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;PSUBSCRIBE news.*       # news.sports, news.weather 등 모두 매칭
PSUBSCRIBE user.??.log  # 정확히 2글자 사이에 있는 패턴 매칭&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 반드시 주의해야할 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 내부적으로 일반 구독과 패턴 구독은 독립적으로 관리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 채널에 대해 SUBSCRIBE와 PSUBSCRIBE를 동시에 적용했다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 채널에 메시지가 발행될 때 메시지를 두 번 수신하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 수신을 피하고자 한다면 구독 방식을 일관되게 유지해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메시지 발행 - PUBLISH&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUBLISH 명령어로 특정 채널에 메시지를 발행하면 해당 채널을 구독 중인 모든 클라이언트에게 메시지가 전송됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776223412352&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;PUBLISH news.sports &quot;오늘 경기 결과: 3-1 승리&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 값은 메시지를 수신한 구독자 수가 됩니다. 이 수는 일반 구독자 수와 패턴 매칭으로 수신한 구독자 수를 합산한 값이 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 내부 구현 - 자료 구조와 시간 복잡도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 Pub/Sub을 내부적으로 어떻게 구현하는지 이해하면 성능 관점에서의 설계 판단이 조금 더 명확해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;두가지 핵심 자료 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Pub/Sub 구현을 위해 두 가지 자료구조를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) pubsub_channels : 해시테이블&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키는 채널 이름, 값은 해당 채널을 구독하는 클라이언트들의 연결리스트 (LinkedList)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 SUBSCRIBE가 이 구조를 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) pubsub_patterns : 연결 리스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 노드가 패턴 문자열과 해당 패턴을 구독하는 클라이언트 리스트를 갖는 연결리스트 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PSUBSCRIBE가 이 구조를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;PUBLISH 명령 실행 시 내부 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUBLISH 명령이 실행되면 Redis는 아래 두 단계를 순서대로 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step1 : 정확한 채널 매칭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pubsub_channels 해시테이블에서 채널 이름으로 조회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시테이블 특성상 시간 복잡도는 O(1)입니다.&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;구독자가 N명이라면 이 단계는 O(N)이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step2 : 패턴 매칭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pubsub_patterns 연결 리스트 전체를 처음부터 끝까지 순회하며 패턴 매칭을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴이 P개이고 문자열 비교 비용이 평균 M이라면, 이 단계의 복잡도는 O(P*M)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴이 매칭되면 해당 구독자들에게 동일하게 메시지를 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 패턴 구독은 일반 구독보다 훨씬 비싼 연산입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 패턴 리스트를 순회해야 하기 때문에 등록된 패턴이 많을수록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUBLISH 비용이 선형으로 증가합니다. 패턴 구독은 정말 필요한 경우에만, 최소한으로 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메모리 사용 관점에서의 핵심&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub에서 메모리와 관련해 가장 중요한 특성은 두 가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 메시지를 저장하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성이 없습니다. 메시지는 전송되는 순간 소멸됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 채널은 구독자가 있을 때만 메모리에 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자가 없는 채널은 pubsub_channels에서 제거됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널이 1,000개 있더라도 아무도 구독하지 않으면 메모리 사용량은 0에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub이 가벼운 핵심적인 이유가 바로 여기있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 메시지를 저장하지 않을까?&lt;/span&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;Pub/Sub이 추구하는 방향은 성능이 우선입니다.&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;Disk I/O 발생&amp;nbsp;&amp;rarr; 병목 현상&lt;/li&gt;
&lt;li&gt;메모리 할당과 해제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; GC 오버헤드&lt;/li&gt;
&lt;li&gt;ACK 메커니즘 구현의 필요성&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 비즈니스 로직 복잡도 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub은 이 모든 것을 생략해 &lt;b&gt;빠른 메시지 전파에만 집중&lt;/b&gt;하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 구독자가 없는 상태에서 메시지를 발행하면, Redis는 전달을 시도조차 하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 메시지 큐처럼 큐에 쌓아두고 나중에 전달한다는 개념 자체가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 파티션이 발생한 경우도 마찬가지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis는 전송 실패를 감지할 수 있지만, Pub/Sub에는 따로 Retry 정책이 없고 누락된 메시지를 복구하는 것도 불가&lt;/b&gt;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 분산환경에서의 Pub/Sub : 클러스터와 레플리카&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Redis 클러스터에서의 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 클러스터에서 일반 키는 해시슬롯에 따라 특정 노드에 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Pub/Sub은 다릅니다. 구독자가 어느 노드에 연결되어 있는지 알 수 없기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 모든 노드에 브로드캐스트해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행자가 노드 A에 연결되어 있고 구독자가 노드 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;이를 위해 Redis는 클러스터 버스(Cluster Bus)를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 버스는 노드 간 통신을 위한 별도의 TCP 연결로, 일반적으로 기본 포트에 10,000을 더한 포트를 사용하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gossip 프로토콜 기반의 바이너리 프로토콜로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUBLISH가 실행되면 아래의 순서로 동작합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;발행 노드가 자신에게 연결된 로컬 구독자들에게 즉시 메시지를 전달합니다.&lt;/li&gt;
&lt;li&gt;이후 클러스터 버스를 통해 다른 모든 노드로 메시지를 전파합니다.&lt;/li&gt;
&lt;li&gt;각 노드는 수신한 메시지를 자신의 로컬 구독자들에게 전달합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 문제가 바로 네트워크 대역폭 증폭입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드가 N개인 클러스터에서 1KB 메시지를 초당 10,000개 발행하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 노드 기준으로는 10MB/s의 트래픽이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 10개 노드 클러스터라면 모든 노드 합산 기준으로 100MB/s가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 규모가 커질수록 Pub/Sub 트래픽은 N배로 증폭됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Redis 7.0의 해답 : Sharded Pub/Sub&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 Redis 7.0에서 Sharded Pub/Sub이 도입되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPUBLISH와 SSUBSCRIBE 명령을 사용하며 채널이 해시 슬롯에 할당됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1776224740586&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SSUBSCRIBE news.sports   # 특정 샤드의 구독자만 수신
SPUBLISH news.sports &quot;경기 시작&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널이 담당하는 샤드의 노드에만 메시지가 전달되므로 네트워크 트래픽이 N배에서 상수 수준으로 대폭 줄어들게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 TradeOff가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채널이 특정 슬롯에 매핑되며, 해당 슬롯을 담당하는 노드에서만 메시지가 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 클라이언트가 올바른 노드에 연결해야하며, 클러스터 리샤딩이 발생하면 구독이 끊어질 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;마스터-레플리카 환경에서의 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터-레플리카 구조에서 일반 키는 마스터에서 레플리카로 복제됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Pub/Sub 메시지는 복제되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터에서 발행된 메시지는 마스터에 연결된 구독자들에게만 전파됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카로는 전달되지 않습니다. 이유는 단순합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제가 의미 있으려면 과거 메시지 기록과 안정성이 전제되어야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Pub/Sub의 설계 철학은 바로 지금 이 순간에만 집중하는 것입니다.&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;따라서 실무에서 Pub/Sub을 활용할 때는 모든 Pub/Sub 클라이언트를 마스터에 연결해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카에 구독자를 연결하면 메시지를 수신하지 못하게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub을 공부해보며 느낀 점은 Redis는 정말 뚜렷한 철학을 가지고 설계되었다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;빠르게 전파하되 책임은 지지 않는다.&quot; 메시지를 저장하지 않고 Retry도 없고 복제도 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 선택들이 Redis의 설계 철학을 더 뚜렷하게 보여주는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub을 처음 접했을 때는 &quot;왜 메시지를 저장 안하지?&quot;라는 의문이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내부 구조를 이해하고, Redis의 철학에 대해 공부해보고 나면서 납득이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, Redis Pub/Sub은 &lt;b&gt;유실을 허용할 수 있는 실시간 이벤트 전파&lt;/b&gt;에 최적화된 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서 생각해보면 알림, 실시간 피드, 캐시 무효화 이벤트처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 메시지가 누락되어도 서비스에 치명적이지 않은 곳에 어울립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 결제, 재고 변경, 주문 처리처럼 단 하나의 메시지 유실도 허용할 수 없는 도메인이라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Streams나 Kafka처럼 영속성을 보장하는 시스템을 선택해야 할 것 같습니다.&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/44</guid>
      <comments>https://xeulbn-dev.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 15 Apr 2026 13:04:15 +0900</pubDate>
    </item>
    <item>
      <title>(CS Study) Redis의 핵심 - Key 관리 기법과 영속성</title>
      <link>https://xeulbn-dev.tistory.com/43</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;  들어가기 전에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 단순 캐시로만 사용할 때는 사실 데이터가 날아가도 큰 문제가 없습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 세션, 장바구니, 게임 진행 상태, 재고 상태 처럼 비즈니스에 중요한 데이터를 Redis에 보관하는 순간부터는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날아가는 순간, 큰일납니다. 서버가 재시작 되거나, 전원이 꺼진다면? 데이터가 모두 증발해버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 수백만 개의 키가 쌓인 운영환경에서 특정 키를 조회하겠다고 잘못된 명령 하나를 실행했다가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 전체가 멈추는 장애로 이어질 수도 있습니다. (실제 아래 쿠팡 사례, 카카오 기사 참조)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zdnet.co.kr/view/?no=20131119174125&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://zdnet.co.kr/view/?no=20131119174125&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775814393240&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;카카오 &amp;quot;레디스, 잘못쓰면 망한다&amp;quot;&quot; data-og-description=&quot;국민메신저 카카오톡 개발 업체인 카카오가 웹애플리케이션 서비스를 만드는 개발자들을 대상으로 오픈소스 기술 '레디스(Redis)' 활용 경험을 소개했다. 우수 활용사례가 아니라 절대 하면 안 &quot; data-og-host=&quot;zdnet.co.kr&quot; data-og-source-url=&quot;https://zdnet.co.kr/view/?no=20131119174125&quot; data-og-url=&quot;https://zdnet.co.kr/view/?no=20131119174125&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b89l4w/dJMb8TCaG2G/agQfqNBzg1VAkSosQyn4k0/img.jpg?width=480&amp;amp;height=320&amp;amp;face=216_82_276_147,https://scrap.kakaocdn.net/dn/MMGhZ/dJMb8ZvCwCr/xQSHMu9Bg2W2Zam0OPWR11/img.jpg?width=480&amp;amp;height=320&amp;amp;face=216_82_276_147,https://scrap.kakaocdn.net/dn/bSyR84/dJMb8RRSW4E/rT0sHyXAVJQebgPYGcjxA0/img.jpg?width=200&amp;amp;height=200&amp;amp;face=58_55_133_137&quot;&gt;&lt;a href=&quot;https://zdnet.co.kr/view/?no=20131119174125&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://zdnet.co.kr/view/?no=20131119174125&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b89l4w/dJMb8TCaG2G/agQfqNBzg1VAkSosQyn4k0/img.jpg?width=480&amp;amp;height=320&amp;amp;face=216_82_276_147,https://scrap.kakaocdn.net/dn/MMGhZ/dJMb8ZvCwCr/xQSHMu9Bg2W2Zam0OPWR11/img.jpg?width=480&amp;amp;height=320&amp;amp;face=216_82_276_147,https://scrap.kakaocdn.net/dn/bSyR84/dJMb8RRSW4E/rT0sHyXAVJQebgPYGcjxA0/img.jpg?width=200&amp;amp;height=200&amp;amp;face=58_55_133_137');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;카카오 &quot;레디스, 잘못쓰면 망한다&quot;&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;국민메신저 카카오톡 개발 업체인 카카오가 웹애플리케이션 서비스를 만드는 개발자들을 대상으로 오픈소스 기술 '레디스(Redis)' 활용 경험을 소개했다. 우수 활용사례가 아니라 절대 하면 안&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;zdnet.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(카카오 기사)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212880&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212880&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775814474904&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;쿠팡, 전체 품절?...시스템 오류로 주문 불가 - 디지털투데이 (DigitalToday)&quot; data-og-description=&quot;전자상거래업체 쿠팡에서 판매 중인 모든 상품이 24일 오전 '품절'로 뜨며 주문에 어려움을 빚고 있다. 쿠팡은 오류가 발생한 원인을 파악 중이라고 밝혔다.쿠팡은 오늘 오전부터 판매 중인 모든&quot; data-og-host=&quot;www.digitaltoday.co.kr&quot; data-og-source-url=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212880&quot; data-og-url=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212880&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lEUXN/dJMb82MD9K3/DQKwYAtQfjchoCCBMIJ5p0/img.png?width=600&amp;amp;height=1200&amp;amp;face=0_0_600_1200,https://scrap.kakaocdn.net/dn/jwdj2/dJMb85vP3Qk/u09qcUOhb7KoQbre9b73H1/img.png?width=600&amp;amp;height=1200&amp;amp;face=0_0_600_1200&quot;&gt;&lt;a href=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212880&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212880&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lEUXN/dJMb82MD9K3/DQKwYAtQfjchoCCBMIJ5p0/img.png?width=600&amp;amp;height=1200&amp;amp;face=0_0_600_1200,https://scrap.kakaocdn.net/dn/jwdj2/dJMb85vP3Qk/u09qcUOhb7KoQbre9b73H1/img.png?width=600&amp;amp;height=1200&amp;amp;face=0_0_600_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;쿠팡, 전체 품절?...시스템 오류로 주문 불가 - 디지털투데이 (DigitalToday)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;전자상거래업체 쿠팡에서 판매 중인 모든 상품이 24일 오전 '품절'로 뜨며 주문에 어려움을 빚고 있다. 쿠팡은 오류가 발생한 원인을 파악 중이라고 밝혔다.쿠팡은 오늘 오전부터 판매 중인 모든&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.digitaltoday.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;이번 글에서는 그렇기에 Redis 운영에서 반드시 알고가야 할 두 가지 주제인 키 관리 기법과 데이터 영속성을 기술적으로 정리해보려 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  본론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Redis 키 관리 - KEYS와 SCAN&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) KEYS : 절대 production에서 쓰면 안되는 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서 key를 검색하는 가장 직관적인 명령어는 KEYS입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;glob 스타일의 패턴 매칭을 지원하며 와일드 카드까지 사용이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775814563555&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;KEYS *                -- 모든 키 반환
KEYS user:*           -- &quot;user:&quot;로 시작하는 모든 키 반환
KEYS user:[123]*      -- user:1, user:2, user:3으로 시작하는 키 매칭
KEYS user:[a-z]*      -- user: 다음에 소문자 알파벳이 오는 키 매칭
KEYS user:\*          -- &quot;user:*&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;문제는 성능입니다. KEYS는 패턴에 맞는 키를 찾기 위해 데이터베이스의 전체 키를 순회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간복잡도가 O(n)이며, 여기서 n은 전체 키 개수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 심각한 문제는 Blocking에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 싱글 스레드로 동작하기 때문에, KEYS가 실행되는 동안 다른 모든 요청을 대기해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만개의 키를 순회하는 동안 다른 클라이언트는 응답을 받지 못하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 쿠팡 장애의 원인이었고, Redis 공식 문서도 production 환경에서의 사용을 명시적으로 경고하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212904&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212904&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775814714576&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;쿠팡 오류 원인은 오픈소스 '레디스 DB' 때문 - 디지털투데이 (DigitalToday)&quot; data-og-description=&quot;쿠팡의 서비스 오류 원인이 &amp;lsquo;레디스 DB&amp;rsquo; 문제인 것으로 드러났다.24일 오전 7시경부터 쿠팡 판매 상품의 재고가&amp;lsquo;0&amp;rsquo;으로 표시돼, 소비자는 관련 상품의 주문 및 구매할 수 없었다. 이에 쿠팡 &quot; data-og-host=&quot;www.digitaltoday.co.kr&quot; data-og-source-url=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212904&quot; data-og-url=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212904&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bl5DUb/dJMb87NXvWF/rdXtMD31At6Qmquw6wlcv1/img.jpg?width=600&amp;amp;height=211&amp;amp;face=127_68_189_130,https://scrap.kakaocdn.net/dn/dhOfuK/dJMb9eTQJSw/qquznz9ImpntNtFUfdOB71/img.jpg?width=600&amp;amp;height=211&amp;amp;face=127_68_189_130&quot;&gt;&lt;a href=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212904&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.digitaltoday.co.kr/news/articleView.html?idxno=212904&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bl5DUb/dJMb87NXvWF/rdXtMD31At6Qmquw6wlcv1/img.jpg?width=600&amp;amp;height=211&amp;amp;face=127_68_189_130,https://scrap.kakaocdn.net/dn/dhOfuK/dJMb9eTQJSw/qquznz9ImpntNtFUfdOB71/img.jpg?width=600&amp;amp;height=211&amp;amp;face=127_68_189_130');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;쿠팡 오류 원인은 오픈소스 '레디스 DB' 때문 - 디지털투데이 (DigitalToday)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;쿠팡의 서비스 오류 원인이 &amp;lsquo;레디스 DB&amp;rsquo; 문제인 것으로 드러났다.24일 오전 7시경부터 쿠팡 판매 상품의 재고가&amp;lsquo;0&amp;rsquo;으로 표시돼, 소비자는 관련 상품의 주문 및 구매할 수 없었다. 이에 쿠팡&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.digitaltoday.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(쿠팡 기사)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-10 오후 6.53.21.png&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6JfZ/dJMcagrzuMb/js8WBLv9GDkuJKBqwRhVn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6JfZ/dJMcagrzuMb/js8WBLv9GDkuJKBqwRhVn0/img.png&quot; data-alt=&quot;Redis 공식문서 KEYS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6JfZ/dJMcagrzuMb/js8WBLv9GDkuJKBqwRhVn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6JfZ%2FdJMcagrzuMb%2Fjs8WBLv9GDkuJKBqwRhVn0%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;747&quot; height=&quot;145&quot; data-filename=&quot;스크린샷 2026-04-10 오후 6.53.21.png&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis 공식문서 KEYS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(출처 :&amp;nbsp; &lt;a href=&quot;https://redis.io/docs/latest/commands/keys/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redis.io/docs/latest/commands/keys/&lt;/a&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;KEYS는 로컬 개발 환경에서 데이터를 확인하는 용도로만 사용해볼 수 있을 것 같다라는 생각이 들었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2) SCAN : KEYS의 대안&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCAN은 커서기반으로 키를 조금씩 나눠서 반환하는 방법입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KEYS의 문제를 근본적으로 해결합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1775814964138&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동작 방식은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;첫 호출은 cursor 0으로 시작하고, Redis는 일부 키와 함께 다음 커서 값을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;반환 형식은 두 요소의 배열로, 첫 번째 요소가 다음 커서, 두 번째 요소가 키 배열입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/10vML/dJMcabcHYBO/cxfwFKbeu3YKckHkaRNClk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/10vML/dJMcabcHYBO/cxfwFKbeu3YKckHkaRNClk/img.png&quot; data-alt=&quot;Step 1 (시작)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/10vML/dJMcabcHYBO/cxfwFKbeu3YKckHkaRNClk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F10vML%2FdJMcabcHYBO%2FcxfwFKbeu3YKckHkaRNClk%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;923&quot; height=&quot;396&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Step 1 (시작)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eFBJOV/dJMcacJvtYx/vlsehed9vZJIWKZBnGTKT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eFBJOV/dJMcacJvtYx/vlsehed9vZJIWKZBnGTKT0/img.png&quot; data-alt=&quot;Step 2 (커서 기반으로 다음 호출)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eFBJOV/dJMcacJvtYx/vlsehed9vZJIWKZBnGTKT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeFBJOV%2FdJMcacJvtYx%2Fvlsehed9vZJIWKZBnGTKT0%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;914&quot; height=&quot;479&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Step 2 (커서 기반으로 다음 호출)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O9U4w/dJMcagyj7V3/RmBfVUwAZKUYcCAGClUYB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O9U4w/dJMcagyj7V3/RmBfVUwAZKUYcCAGClUYB1/img.png&quot; data-alt=&quot;Step 3 (0에 도달하면 순회 완료!)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O9U4w/dJMcagyj7V3/RmBfVUwAZKUYcCAGClUYB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO9U4w%2FdJMcagyj7V3%2FRmBfVUwAZKUYcCAGClUYB1%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;908&quot; height=&quot;555&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Step 3 (0에 도달하면 순회 완료!)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서가 위처럼 0으로 돌아오면 전체 순회가 끝난 것입니다. 각 호출의 시간복잡도는 COUNT에 비례하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT는 보통 작은 상수이므로 실질적으로 O(1)에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만 개의 키가 있어도 SCAN은 한 번에 수십 개씩 처리하기 때문에 다른 요청 처리 사이사이에 실행이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, SCAN 사용시 주의할 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDB에서 Cursor 기반 페이징 조회를 생각해보면 유사합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 키가 반환될 수 있습니다. 순회 도중 키가 추가,삭제 되거나, Redis 내부 해시테이블이 리사이징 되면서 키 위치가 변경되면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 키가 여러 번 반환될 수 있습니다. 따라서 SCAN 결과를 처리할 때는 중복 처리 로직이 필요합니다.&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;size18&quot;&gt;3) EXISTS : 키 존재 여부 확인&lt;/p&gt;
&lt;pre id=&quot;code_1775815557873&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXISTS key [key ...]&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;키가 존재하면 1, 존재하지 않으면 0을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 3.0.3부터는 여러 키를 한 번에 확인할 수 있으며 존재하는 키의 개수를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 키를 여러번 지정하면 중복 카운트되므로&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775815600965&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXISTS mykey mykey mykey&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;를 실행하게 되면, mykey가 존재한다면 3을 반환하게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키의 존재 여부만 확인할때는 GET으로 nil여부를 확인하는 것보다는 EXISTS명령어가 더 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도가 명확하고, String 타입이 아닌 자료구조에도 사용 가능하며 값 전체를 전송하지 않아 네트워크 관점에서도 효율적입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Redis 영속성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1) 영속성이 필요한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 가장 큰 특징은 모든 데이터를 메모리에 저장하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 빠르지만 메모리는 휘발성입니다. 서버가 재시작 되면 모든 데이터가 사라지는 것이지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시작 상황은 생각보다 자주 발생합니다. 보안 패치, Redis 버전 업그레이드, 서버 업데이트,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예기치 않은 전원 문제나 프로세스 종료까지.&lt;/p&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에서 다시 채우면 되니 영속성이 필요 없습니다. 하지만 로그인 세션, 장바구니, 게임 진행 상태, 결제 관련 정보처럼 Redis에만 저장하는 중요 데이터라면 이야기가 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 Redis는 아래 세 가지 영속성 방식을 제공하게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2) 영속성 방식 1 - RDB (스냅샷)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB(Redis Database)는 특정 시점의 메모리 데이터 전체를 하나의 파일로 저장하는 방식입니다.&lt;/p&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;pre id=&quot;code_1775815848417&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dump.rdb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이며, 압축된 바이너리 형태로 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 방식은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSiRrs/dJMcagE8Rpy/1KGHQ3vDFjAft0L0CXdJ80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSiRrs/dJMcagE8Rpy/1KGHQ3vDFjAft0L0CXdJ80/img.png&quot; data-alt=&quot;평상시의 메모리 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSiRrs/dJMcagE8Rpy/1KGHQ3vDFjAft0L0CXdJ80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSiRrs%2FdJMcagE8Rpy%2F1KGHQ3vDFjAft0L0CXdJ80%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;823&quot; height=&quot;428&quot; data-origin-width=&quot;823&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;평상시의 메모리 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 fork 시스템 콜로 자식 프로세스를 생성하고, 자식 프로세스가 스냅샷 파일을 만드는 동안&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 프로세스 (Redis)는 계속 클라이언트의 요청을 처리합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohMJM/dJMcai3WmXU/YqYbLJEEho3V8Gg0gtXJZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohMJM/dJMcai3WmXU/YqYbLJEEho3V8Gg0gtXJZK/img.png&quot; data-alt=&quot;fork 호출시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohMJM/dJMcai3WmXU/YqYbLJEEho3V8Gg0gtXJZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohMJM%2FdJMcai3WmXU%2FYqYbLJEEho3V8Gg0gtXJZK%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;1210&quot; height=&quot;701&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fork 호출시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 Copy-on-Write(CoW)방식을 사용해 fork 직후에는&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;1198&quot; data-origin-height=&quot;713&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjZLdw/dJMcaaEVm1v/omHaT0fpCC1yyNkOfnR9W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjZLdw/dJMcaaEVm1v/omHaT0fpCC1yyNkOfnR9W1/img.png&quot; data-alt=&quot;fork 직후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjZLdw/dJMcaaEVm1v/omHaT0fpCC1yyNkOfnR9W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjZLdw%2FdJMcaaEVm1v%2FomHaT0fpCC1yyNkOfnR9W1%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;1198&quot; height=&quot;713&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;713&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fork 직후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 측에서 쓰기가 발생할 때만 해당 페이지를 복사합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dY2QlS/dJMcacQfmUK/m84DVJEaqul7g0GhgSrKX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dY2QlS/dJMcacQfmUK/m84DVJEaqul7g0GhgSrKX0/img.png&quot; data-alt=&quot;부모 측 쓰기 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dY2QlS/dJMcacQfmUK/m84DVJEaqul7g0GhgSrKX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdY2QlS%2FdJMcacQfmUK%2Fm84DVJEaqul7g0GhgSrKX0%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;1226&quot; height=&quot;734&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;부모 측 쓰기 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 dump.rdb가 생성되는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blBhZG/dJMcadhjAnd/CB7PaKgkdslAIDr7XxpEoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blBhZG/dJMcadhjAnd/CB7PaKgkdslAIDr7XxpEoK/img.png&quot; data-alt=&quot;dump.rdb의 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blBhZG/dJMcadhjAnd/CB7PaKgkdslAIDr7XxpEoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblBhZG%2FdJMcadhjAnd%2FCB7PaKgkdslAIDr7XxpEoK%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;1240&quot; height=&quot;846&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;dump.rdb의 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 실제 메모리 복사 비용이 최소화됩니다.&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;장점으로는 단일 binary 파일이기에 관리가 쉽고 S3같은 외부 스토리지에 날짜별 백업 보관도 용이합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 크기가 작고 서버 재시작시 복구 속도가 빠르며 (binary 파일이기 때문에) BG save라는 백그라운드로 동작하기에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 Redis운영에 미치는 영향이 적습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;또한 fork 시점에 메모리 여유 공간이 필요하고, 데이터 규모가 크면 fork 자체가 느려질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Disk I/O가 발생합니다. (사실 어쩔 수 없는 단점인 것 같습니다 이부분은.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3) 영속성 방식 2 - AOF (Append Only File)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOF는 데이터를 변경하는 모든 쓰기 명령을 로그 파일에 순차적으로 기록하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB가 결과(스냅샷)를 저장한다면 AOF는 과정(명령 이력)을 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SET, LPUSH 같은 쓰기 명령은 기록되지만, GET과 같은 읽기 명령은 데이터를 변경하지 않으므로 기록되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 재시작하면 AOF 파일의 명령을 처음부터 끝까지 재실행하여 다운 직전의 상태를 복원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화 전략이 AOF의 중요한 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일에 쓰는 것과 디스크에 저장하는 것은 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS는 성능을 위해 쓰기를 버퍼링하기 때문에, 디스크에 저장하기 전에 서버가 다운되면 버퍼의 데이터가 사라집니다.&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;always : 매 명령마다 즉시 디스크에 동기화합니다. 가장 안전하지만, 성능이 가장 느립니다.&lt;/li&gt;
&lt;li&gt;everysec : 1초마다 한 번씩 동기화합니다. Redis 공식에서 권장하는 기본값입니다.&lt;br /&gt;최대 1초 분량의 데이터 손실 가능성이 있지만 성능과 안전성의 균형이 좋습니다.&lt;/li&gt;
&lt;li&gt;no : OS가 적절한 시점에 디스크에 저장합니다. 성능은 좋지만 데이터 손실 범위가 큽니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOF는 append-only이기 때문에 시간이 지남에 따라 파일이 수십 GB, 수백 GB까지 커질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 키를 1000번 수정했다면 999개의 중간 명령은 사실상 무의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 Redis는 &lt;b&gt;AOF Rewrite를 통해&amp;nbsp;&lt;/b&gt;불필요한 명령을 제거하고 최소한의 명령만 남깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB와 동일하게 fork를 통해 자식 프로세스가 백그라운드로 실행하며&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;장점은 데이터 손실이 거의 없다는 것입니다. everysec만 사용해도 최대 1초 분량만 손실됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AOF 파일은 텍스트 형식이라 실수로 FLUSHALL을 실행헀다면 서버를 즉시 정지하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOF 파일에서 해당 명령을 삭제한 뒤 재시작하여 복구할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점은 텍스트 파일이기에 압축 바이너리인 RDB보다 파일 크기가 크고, 명령을 재실행하는 방식이라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복구 속도가 느립니다. 또한, always 전략 사용시 디스크 I/O로 인한 성능 저하 또한 따라오게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4) 영속성 방식 3 - 하이브리드 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB는 빠르지만 데이터 손실의 가능성이 있고, AOF는 안전하지만 복구가 느립니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 두 개를 합치면?&lt;/span&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;이라는 발상에서 나온 방식이 Redis 4.0에서 도입된 하이브리드 방식입니다.&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;AOF Rewrite 시 파일 앞부분은 RDB 형식의 압축 바이너리 스냅샷으로, 뒷부분은 AOF 형식의 텍스트 명령어로 구성됩니다.&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;AOF Rewrite가 시작되면, fork로 자식 프로세스를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 프로세스는 현재 메모리 상태를 스캔하여 RDB 형식으로 파일 앞부분에 씁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 부모 프로세스(Redis)는 계속 요청을 처리하며 새로운 명령들을 AOF 파일과 재작성 버퍼 두 곳에 기록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 프로세스가 RDB 쓰기를 마치면 부모로부터 재작성 버퍼를 넘겨받고, 이를 AOF형식으로 파일 뒷부분에 이어 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 원본 파일을 아예 새 파일로 교체합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복구 과정은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 재시작 시 파일 앞부분을 RDB 형식으로 인식해 빠르게 메모리에 로드하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 뒷부분의 AOF 명령들을 순차 재실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOF 부분은 Rewrite 이후 발생한 명령들 뿐이라 매우 극소량입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 대부분의 데이터는 빠른 RDB로딩으로 복구되고, 최신 소수의 명령만 AOF로 재실행됩니다.&lt;/p&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;파일 구조가 복잡해 직접 편집이 어렵고 Redis 4.0미만 버전과는 호환되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(하지만 4.0나온지 어마어마하게 오래된 것으로 알기에...큰 문제가 될 것 같지는 않습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, Rewrite 시 fork를 사용하기 때문에 Copy-on-Write 메모리 오버헤드가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 순수 AOF와 동일한 문제이므로 하이브리드 방식 고유의 단점이라기보다는 상속된 특성으로 봐야할 것 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5) 상황별 영속성 전략 선택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무조건 좋다고 다쓰는 것이 아니라, 어떤 방식을 선택할지는 항상 서비스의 특성과 요구사항에 따라 달라지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a. 영속성이 필요 없는 경우가 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 순수 캐시 용도로만 사용한다면 영속성을 비활성화 할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 다시 채울 수 있는 데이터라면 굳이 디스크 I/O 비용을 감수할 필요가 없을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;b. 정기 백업이 필요한 경우가 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 데이터, 통계 데이터 처럼 약간의 손실이 허용된다면 RDB 방식이 적합할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 파일 관리가 편리하고 S3에 날짜별로 보관하기도 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;c. 데이터 손실을 최소화해야하는 경우가 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 정보, 장바구니 상태, 게임 진행 데이터 처럼 손실이 사용자 경험에 직접적인 영향을 미친다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOF의 everysec전략이 적합할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;d. 빠른 복구와 높은 안정성이 모두 필요한 경우가 있을 것 같습니다.&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;e. 절대적인 안정성이 필요한 경우도 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 거래 데이터나 비즈니스 핵심 데이터라면 AOF와 RDB를 동시에 활성화하는 이중 백업 전략을 고려할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 파일을 모두 생성하여 함께 관리될 수 있도록 하는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KEYS처럼 아무 생각 없이 사용했다가 수백만건을 순회하면서 서비스 전체를 블로킹하는 상황,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 설정 없이 중요한 데이터를 Redis에만 보관했다가 재시작 한 번에 모든 것이 증발하는 상황 등등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 충분히 실제로 일어날 수 있는 일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Redis는 명령어를 단순히 사용하는 것보다 내부적으로 어떻게 동작하고 어떤 부작용을 가져오는지를 이해해야 제대로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영하는데에 문제가 없을 것이라는 점을 다시 한 번 깨닳았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study</category>
      <author>xeulbn-dev</author>
      <guid isPermaLink="true">https://xeulbn-dev.tistory.com/43</guid>
      <comments>https://xeulbn-dev.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 10 Apr 2026 20:02:21 +0900</pubDate>
    </item>
  </channel>
</rss>