<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>JangBaGeum.gif</title>
    <link>https://jangbageum.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 12 May 2026 00:36:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>장바금</managingEditor>
    <image>
      <title>JangBaGeum.gif</title>
      <url>https://tistory1.daumcdn.net/tistory/5065512/attach/651e1ae645134336806a80d6ba17bec5</url>
      <link>https://jangbageum.tistory.com</link>
    </image>
    <item>
      <title>나만의? DB 모델링 원칙 - MySQL 편</title>
      <link>https://jangbageum.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개발자로 일하면 가장 오래 고민했던 주제 중 하나가 &lt;b&gt;DB 모델링 컨벤션&lt;/b&gt;이다. 코드에는 ESLint, Prettier 같은 도구가 있어서 팀원 간 스타일을 자동으로 맞출 수 있지만, DB 설계에는 그런 자동화 도구가 없는 것으로 보인다. 결국 &lt;b&gt;사람이 규칙을 정하고, 사람이 지켜야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 글은 여러 프로젝트를 거치면서 정리한 나만의 MySQL 모델링 컨벤션을 정리하려고 한다. 요즘 AI를 통한 개발뿐만 아니라 설계도 함께하면서 사소한 컨벤션, 구두로 전해 내려오던 도메인적 규칙 등 모두 콘텍스트에 녹일 수 있도록 정리를 하고 있다. 이도 그 과정 중에 하나이다. 절대적인 정답은 아니고, 실무에서 반복적으로 부딪히며 다듬어 온 기준이다. 팀마다, 도메인마다 맞지 않는 부분이 있을 수 있으니 참고용으로 해도 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 명명 규칙&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;명명 규칙은 컨벤션의 가장 기본이면서, 동시에 가장 효과가 큰 영역이다. 이름만 보고도 이 테이블이 뭔지, 이 컬럼이 어떤 역할인지 알 수 있다면 별도의 문서 없이도 스키마를 읽을 수 있다. 내가 생각하는 최고의 네이밍은 부가적인 설명 없이 역할을 바로 알 수 있는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1.1 공통 규칙&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;a. 모든 네이밍은 snake_case를 사용한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776595279174&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;memberList  (X)
member_list (O)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이유는 단순하다. MySQL은 OS에 따라 &lt;b&gt;대소문자 처리 방식이 다르다.&lt;/b&gt; Linux에서는 대소문자를 구분하지만 macOS나 Windows에서는 구분하지 않는 경우가 있다. 로컬 개발 환경(macOS)에서는 문제없이 동작하다가 운영 서버(Linux)에서 테이블을 못 찾는 상황이 실제로 발생한다. snake_case + 소문자 통일이면 이런 문제를 원천 차단할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;b. 직관적인 기술형으로 작성한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이름만 보고도 어떤 데이터인지 명확히 알 수 있어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가능한 쉬운 단어를 선택하고, 모호한 이름은 피한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776595373724&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;log           (X) &amp;mdash; 어떤 로그인지 불명확
play_log      (O) &amp;mdash; 재생 로그임이 명확
access_log    (O) &amp;mdash; 접속 로그임이 명확&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;c. 동사는 능동태를 사용한다. (동명사는 허용)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776595420162&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;created_date  (X)
create_date   (O)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;과거분사 대신 능동태를 쓰면 네이밍이 일관되고 짧아진다. `created_at`이 더 자연스러운 영어라는 의견도 있지만, 컬럼명의 핵심은 &lt;b&gt;팀 내 일관성&lt;/b&gt;이다. 어떤 쪽을 선택하든 섞어 쓰지만 않으면 된다. 나는 능동태 쪽을 택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;d. 데이터베이스 예약어는 사용하지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`order`, `group`, `status` 같은 단어는 MySQL 예약어이거나 예약어에 가깝다. 백틱(`` ` ``)으로 감싸면 쓸 수 있지만, ORM이나 쿼리 빌더에서 문제를 일으킬 수 있다. 처음부터 피하는 게 정신건강에 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;e. 약어는 최소한으로 사용한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;약어는 작성자에게는 당연하지만, 새로 합류한 팀원에게는 퍼즐이다. 꼭 필요한 경우에만, 팀 내에서 합의된 약어만 사용한다.&lt;/span&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;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;대상&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;약어&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;number&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;no&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;episode_no, sort_no&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;count&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;cnt&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;play_cnt, like_cnt&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;address&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;addr&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;billing_addr, ip_addr&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;image&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;img&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;thumbnail_img, profile_img&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;authentication&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;auth&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;member_auth&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;maximum&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;max&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;duration_max, retry_max&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;minimum&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;min&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;price_min, age_min&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;quantity&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;qty&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;payment_ref, external_ref&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;transaction&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;tx&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;tx_id, tx_status&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1.2 테이블 명명 규칙&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;a. 접두사/접미사는 사용하지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776608416031&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tb_content   (X)
content_tbl  (X)
content      (O)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`tb_`, `tbl_` 같은 접두사는 정보량이 제로다. DB 안에 있으면 테이블이라는 건 자명하다. 불필요한 접두사는 이름만 길게 만들 뿐이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단, &lt;b&gt;마스터 테이블의 하위 속성 테이블&lt;/b&gt;은 마스터 테이블명을 접두사로 사용한다. 이건 관계를 명확히 표현하기 위함이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776608527164&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;member           &amp;mdash; 회원 마스터
member_auth      &amp;mdash; 회원 인증 정보
member_profile   &amp;mdash; 회원 프로필
content          &amp;mdash; 콘텐츠 마스터
content_like     &amp;mdash; 콘텐츠 좋아요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;b. 테이블 이름은 단수형으로 사용한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776608552608&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;contents (X)
content  (O)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;복수형을&amp;nbsp;쓰면&amp;nbsp;불규칙&amp;nbsp;복수형&amp;nbsp;문제가&amp;nbsp;따라온다.&amp;nbsp;`category`&amp;nbsp;&amp;rarr;&amp;nbsp;`categories`,&amp;nbsp;`person`&amp;nbsp;&amp;rarr;&amp;nbsp;`people`.&amp;nbsp;단수형&amp;nbsp;통일이&amp;nbsp;훨씬&amp;nbsp;깔끔하다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1.3 컬럼 명명 규칙&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컬럼은 &lt;b&gt;타입에 따른 접미사를 통일&lt;/b&gt;하는 것이 핵심이다. 접미사만 보고도 이 컬럼이 어떤 성격의 데이터인지 파악할 수 있다.&lt;/span&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;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;용도&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;패턴&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;PK&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;테이블명&amp;gt;_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;member_id, content_id&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;FK&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;부모 테이블명&amp;gt;_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;member_id, genre_id&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;날짜 (시간 없음)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;목적&amp;gt;_date&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;release_date, expire_date&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;날짜 + 시간&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;목적&amp;gt;_at&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;create_at, update_at, delete_at&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드/유형&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;목적&amp;gt;_code&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;quality_code, region_code&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;순번&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;목적&amp;gt;_no&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;episode_no, sort_no&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Boolean&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;is_&amp;lt;목적&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;is_active, is_adult&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 규칙의 장점은 &lt;b&gt;IDE의 자동완성과 궁합이 좋다&lt;/b&gt;는 것이다. `is_`를 타이핑하면 Boolean 컬럼 목록이 쭉 나오고, `_at`으로 검색하면 시간 관련 컬럼만 필터링된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1.4 인덱스 명명 규칙&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776608991377&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;형식: &amp;lt;table_name&amp;gt;_&amp;lt;column1&amp;gt;_&amp;lt;column2&amp;gt;_..._&amp;lt;접미사&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인덱스 타입&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;접미사&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;일반 인덱스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;_IDX&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;content_genre_id_create_at_IDX&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;유니크 인덱스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;_UIDX&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;member_email_UIDX&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Fulltext 인덱스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;_FTX&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;content_title_FTX&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;복합 인덱스라면 &lt;b&gt;실제 쿼리에서 사용되는 조건 순서대로&lt;/b&gt;&amp;nbsp;컬럼을 나열한다. 이렇게 하면 인덱스명 자체가 일종의 문서 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776609160846&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- content 테이블에서 genre_id와 create_at으로 자주 조회한다면
CREATE INDEX content_genre_id_create_at_IDX ON content (genre_id, create_at);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 테이블 타입 전략&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;데이터 타입 선택은 단순히 &quot;돌아가면 되지&quot;의 영역이 아니다. &lt;b&gt;타입 하나의 차이가 수억 건의 데이터에서는 GB 단위의 저장 공간 차이&lt;/b&gt;로 이어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.1 원칙: 가장 작은 데이터 타입을 사용하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;저장 공간&lt;/b&gt;: 1억 행 기준, `TINYINT`(1byte) vs `INT`(4byte) = 100MB vs 400MB&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;인덱스 크기&lt;/b&gt;: 작은 타입 &amp;rarr; 작은 인덱스 &amp;rarr; 버퍼 풀에 더 많이 적재 &amp;rarr; 검색 속도 향상&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;메모리&lt;/b&gt;: MySQL이 데이터를 처리할 때 필요한 메모리가 줄어든다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;네트워크&lt;/b&gt;: 데이터 전송 시 필요한 대역폭이 줄어든다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;나중에 범위가 늘어나면 어떡하지?&quot;라는 걱정에 무조건 `BIGINT`를 쓰는 경우를 종종 본다. 하지만 &lt;b&gt;현재 요구사항에 맞는 타입을 선택하고, 필요할 때 마이그레이션하는 것&lt;/b&gt;이 올바른 접근이다. 미래의 불확실성을 위해 현재의 비용을 지불할 필요는 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.2 숫자 타입 선택 기준&lt;/span&gt;&lt;/b&gt;&lt;/h3&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;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;데이터 범위&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;권장 타입&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저장 공간&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-128 ~ 127&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TYNYINT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1byte&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-32,768 ~ 32,767&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SMALLINT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-8,388,608 ~ 8,388,607&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MEDIUMINT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-2^31 ~ 2^31-1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;INT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-2^63 ~ -2^63-1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;BIGINT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 음수를 사용하지 않는 컬럼에는 &lt;b&gt;UNSIGNED&lt;/b&gt; 옵션을 사용한다. 양수 범위가 두 배가 확장된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 금액 데이터는 반드시 &lt;b&gt;DECIMAL&lt;/b&gt;을 사용한다. FLOAT나 DOUBLE은 부동소수점 오차가 있어서 정산 데이터에 쓰면 돈이 증발할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777605239406&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 구독료 컬럼
subscription_fee DECIMAL(10,2) UNSIGNED NOT NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.3 문자열 타입 선택 기준&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%; height: 79px;&quot; border=&quot;1&quot; data-line=&quot;165&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;타입&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;특성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;사용 시점&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;167&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;CHAR(n)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;고정 길이, 빠른 검색&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;길이가 확실한 짧은 데이터 (status code, 'Y'/'N')&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;168&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;VARCHAR(n)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;가변 길이, 공간 효율적&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;길이가 다양한 일반 문자열&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;169&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;TEXT&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;가변 길이, 포인터 참조&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;인덱스 불필요한 대용량 텍스트&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TEXT 계열 타입은 용량에 따라 세분화 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;173&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;MySQL text type&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;최대 용량&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;175&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;TINYTEXT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;256 bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;176&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;TEXT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;약 64KB&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;177&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;MEDIUMTEXT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;약 16MB&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;178&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;LONGTEXT&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;약 4GB&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실무에서 자주 하는 실수 하나. &lt;b&gt;VARCHAR(65535)로 때우는 경우가 있다&lt;/b&gt;. 이런 경우 차라리 TEXT를 쓰는 것이 낫다. MySQL은 VARCHAR가 일정 크기를 초과하면 내부적으로 임시 테이블을 디스크에 생성하는데, TEXT는 이 비용이 상대적으로 작다. 단 검색이나 정렬이 필요한 필드는 VARCHAR가 맞다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.4 날짜/시간 타입&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;184&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;특성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;DATETIME&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;TIMESTAMP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;186&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;저장 형식&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;'YYYY-MM-DD HH:MM:SS'&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;유닉스 타임스탬프&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;187&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;범위&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;1000-01-01 ~ 9999-12-31&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;1970-01-01 ~ 2038-01-19&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;188&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;시간대&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;시간대와 무관&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;서버 시간대에 따라 변환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;189&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;저장 공간&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;8 bytes&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Serif KR';&quot;&gt;4 bytes&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;나의 선택은 DATETIME이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;TIMESTAMP는 저장 공간이 절반이라 장점이 있지만, &lt;b&gt;2038년 문제&lt;/b&gt;가 있다. 2038년은 아직 멀어 보이지만, 서비스의 수명은 예측할 수 없다. 또한 TIMESTAMP는 서버 시간대에 의존하기 때문에, DB 서버의 시간대 설정이 바뀌면 기존 데이터의 의미가 달라질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;글로벌 서비스에서 시간대 변환이 필요한 경우라면 TIMESTAMP가 우리할 수 있지만, 그런 경우에도 &lt;b&gt;애플리케이션 레벨에서 UTC로 통일하고 DATETIME에 저장하는 방식&lt;/b&gt;이 더 명시적이고 안전하다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.5 Boolean 처리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MySQL의 BOOLEAN은 내부적으로 TINYINT(1)이다. 0과 1로 처리되는데, 이게 언어에 따라 미묘한 문제를 일으킬 수 있다. 예를 들어 일부 언어에서 0을 null이나 false로 해석하는 방식이 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나는&lt;b&gt;TINYINT(1)을 기본으로 사용&lt;/b&gt;하되, ORM을 통해 boolean으로 매핑하는 방식을 선호한다. CHAR(1)에 'Y'/'N'을 넣는 방식도 실무에서 자주 보이지만, 이건 취향의 영역 같다. 아주 중요한 것은&lt;b&gt; 프로젝트 내에서 일관성을 갖는 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777606570347&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- TINYINT 방식
is_adult TINYINT(1) NOT NULL DEFAULT 0

-- CHAR(1) 방식 (대안)
is_adult CHAR(1) NOT NULL DEFAULT 'N'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6 기타 데이터 저장 Tip&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;IP 주소는 문자열 대신 정수로 저장하라.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777606722417&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 저장: 문자열 &amp;rarr; 정수 변환
INSERT INTO access_log (ip_address) VALUES (INET_ATON('192.168.0.1'));

-- 조회: 정수 &amp;rarr; 문자열 변환
SELECT INET_NTOA(ip_address) FROM access_log;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VARCHAR(15)로 저장하면 최대 15바이트지만, INT UNSIGNED로 변환하면 4바이트로 충분하다. 1억 건이면 약 1GB 차이다. 게다가 정수 비교가 문자열 비교보다 빠르고 IP형식의 무결성도 보장된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;UUID는 바이너리로 저장하라.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777606809804&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 저장
INSERT INTO member (uuid) VALUES (UUID_TO_BIN(UUID(), 1));

-- 조회
SELECT BIN_TO_UUID(uuid, 1) FROM member;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;UUID를 VARCHAR(36)으로 저장하면 36바이트지만, BINARY(16)으로 변환하면 16바이트다. UUID_TO_BIN의 두 번째 인자 1은 시간 기반 부분을 앞으로 재배치하여 &lt;b&gt;인덱스 설능을 향상한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;UUID보다 ULID를 고려해보라.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;시간순 정렬이 필요하다면 ULID도 고려해보라.&lt;/span&gt;&lt;span&gt;UUID v4는 완전 랜덤이라 PK나 인덱스로 쓰면 페이지 분할이 자주 발생한다. 이게 신결 쓰인다면 ULID가 좋은 대안이 될 수 있다. &lt;/span&gt;&lt;span&gt;128비트 크기는 UUID와 같지만, 앞 48비트가 타임스탬프라 생성된 순서대로 자연스럽게 정렬된다. 즉 &quot;정렬 가능한 UUID&quot;인 셈이다.&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;저장 방식은 UUID와 동일하게 BINARY(16)을 쓰면 되고 생성은 애플리케이션 레이어에서 ULID 라이브러리로 처리한 뒤 바이너리로 변환해 넣는다. 분산 환경에서 PK를 만들거나, 외부 노출용 식별자가 필요한데 Auto Increment의 순번 노출이 부담스러울 때 잘 어울린다. 비슷한 목적의 UUID v7도 최근 표준화되었으니 함께 살펴보면 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. PK 설계 원칙&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3.1 BIGINT UNSIGNED + Auto Increment가 기본이다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MySQL의 PK는 &lt;b&gt;클러스터드 인덱스(Clustered Index)&lt;/b&gt; 다. 이게 무슨 뜻이냐면, PK 순서대로 데이터가 물리적으로 정렬된다는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;만약 PK가 UUID처럼 순서가 없는 랜덤 값이라면, 새 데이터가 삽입될 때마다 &lt;b&gt;물리적 재배열(페이지 분할)&lt;/b&gt; 이 발생한다. 이는 쓰기 성능을 심각하게 저하시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777607181767&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 권장하는 PK 구조
member_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY 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;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;`BIGINT UNSIGNED`의 최댓값은 약 1844 경이다. 1초에 1만 건을 삽입해도 &lt;b&gt;약 5,800만 년&lt;/b&gt; 동안 쓸 수 있다. 충분하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3.2 복합키(Composite Key)를 PK로 쓰지 않는다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;복합키 PK의 문제는 여러 겹이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;인덱스 크기 증가&lt;/b&gt;: 모든 세컨더리 인덱스가 PK를 포함하므로, PK가 크면 모든 인덱스가 커진다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;JOIN 복잡성&lt;/b&gt;: 다른 테이블에서 참조할 때 여러 컬럼을 항상 함께 써야 한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;ORM 호환성&lt;/b&gt;: 대부분의 ORM은 단일 컬럼 PK를 전제로 설계되어 있다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;복합키가 필요한 관계는 &lt;b&gt;유니크 인덱스로 대체&lt;/b&gt;하고, PK는 서로게이트 키(surrogate key)를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777607390974&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 복합키 PK 대신
CREATE TABLE content_like (
	content_like_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    member_id BIGINT UNSIGNED NOT NULL,
    content_id BIGINT UNSIGNED NOT NULL,
    create_at DATETIME NOT NULL, 
    -- 비즈니스 유니크 제약은 유니크 인덱스로
    CONSTRAINT content_like_member_id_content_id_UIDX UNIQUE (member_id, content_id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 컬럼 순서 정하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;테이블 내 컬럼 순서는 기능에 직접적인 영향을 미치지 않지만, &lt;b&gt;스키마의 가독성&lt;/b&gt;에는 큰 영향을 미친다. 팀원이 테이블 구조를 볼 때, 일관된 순서는 인지 부하를 줄여준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내가 사용하는 순서는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. &lt;b&gt;PK&lt;/b&gt; &amp;mdash; 테이블의 핵심 식별자&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. &lt;b&gt;FK&lt;/b&gt; &amp;mdash; 이 테이블이 어떤 테이블과 관계를 맺는지 바로 보인다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. &lt;b&gt;비즈니스 핵심 컬럼&lt;/b&gt; &amp;mdash; 카디널리티가 높고 조건절에 자주 걸리는 컬럼&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. &lt;b&gt;일반 컬럼&lt;/b&gt; &amp;mdash; 조건절에 잘 안 걸리는 데이터&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. &lt;b&gt;대용량 텍스트&lt;/b&gt; &amp;mdash; TEXT, LONGTEXT 등&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. &lt;b&gt;메타 컬럼&lt;/b&gt;&amp;nbsp;&amp;mdash; `create_at`, `update_at`, `delete_at`&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777607593372&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE episode (
	-- 1. PK
    episode_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    -- 2. FK
    series_id BIGINT UNSIGNED NOT NULL,
    -- 3. 비즈니스 핵심
    title VARCHAR(200) NOT NULL,
    episode_no INT UNSIGNED NOT NULL,
    duration_sec INT UNSIGNED NOT NULL DEFAULT 0,
    is_display TINYINT(1) NOT NULL DEFAULT 1,
    -- 4. 일반 컬럼
    release_date DATE NULL,
    -- 5. 대용량 텍스트
    synopsis TEXT NULL,
    -- 6. 메타 컬럼
    create_at DATETIME NOT NULL,
    update_at DATETIME NOT NULL 
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. 인덱스 설계 원칙&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인덱스는 &lt;b&gt;읽기 성능과 쓰기 성능에서 매우 중요하다&lt;/b&gt;. 인덱스를 추가할수록 SELECT는 빨라지지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;INSERT/UPDATE/DELETE는 느려진다. 인덱스가 하나 추가될 때마다 B-Tree 구조를 갱신해야 하기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5.1 기본 원칙&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;a. 필요한 인덱스만 생성한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;혹시 필요할지 모르니 미리 걸어두자&quot;는 접근은 위험하다. 쓰기가 많은 테이블에서 불필요한 인덱스는 INSERT 지연의 주범이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;b. 쓰기 중심 테이블 vs 읽기 중심 테이블을 구분한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;쓰기 중심 (LOG성 테이블)&lt;/b&gt;: 카디널리티가 높은 단일 컬럼 인덱스를 최소한으로&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;읽기 중심 (조회용 테이블)&lt;/b&gt;: 복합 인덱스를 적극 활용하여 응답 속도 보장&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;c. 조건절에 함수를 사용하면 인덱스를 타지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777609282266&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 인덱스 무효화 (X)
SELECT * FROM member WHERE DATE(create_at) = '2024-01-01';

-- 인덱스 활용 (O)
SELECT * FROM member WHERE create_at &amp;gt;= '2024-01-01' AND create_at &amp;lt; '2024-01-02';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;e. `LIKE '%keyword'` 패턴은 Full Table Scan을 유발한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777609362891&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- Full Table Scan (X)
SELECT * FROM content WHERE title LIKE '%드라마'; 

-- 인덱스 활용 가능 (O)
SELECT * FROM content WHERE title LIKE '마블%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;앞에 와일드카드가 오면 인덱스가 있어도 무조건 전체 스캔이다. 문자열 검색이 빈번하다면 &lt;b&gt;Fulltext 인덱스&lt;/b&gt;를 고려하고, 데이터가 정말 많아지면 Elasticsearch 같은 전문 검색 엔진으로 이관하는 것이 맞다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777609414756&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;[MySQL] 자연어 검색 (FULLTEXT)&quot; data-og-description=&quot;기본적으로 검색 쿼리를 작성할 때, 기본적을 와일드카드를 사용하여 LIKE 검색을 이용하는 것을 떠올릴 것이다. 이 방법은 가장 기본적이며 사용하기 쉬워 어렵지 않게 적용을 할 수 있으나, 데&quot; data-og-host=&quot;jangbageum.tistory.com&quot; data-og-source-url=&quot;https://jangbageum.tistory.com/93&quot; data-og-url=&quot;https://jangbageum.tistory.com/93&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bDIC7V/dJMb8QepAmB/XdCkIcAbIyVKvalP2dRlR1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/I0HNG/dJMb9hC4FjW/VaBExPQUApmlT2cibexWek/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cOxJ90/dJMb8QMfqjY/DLBpMuI66PIdwVmtfyRVek/img.jpg?width=3024&amp;amp;height=3024&amp;amp;face=0_0_3024_3024&quot;&gt;&lt;a href=&quot;https://jangbageum.tistory.com/93&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jangbageum.tistory.com/93&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bDIC7V/dJMb8QepAmB/XdCkIcAbIyVKvalP2dRlR1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/I0HNG/dJMb9hC4FjW/VaBExPQUApmlT2cibexWek/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cOxJ90/dJMb8QMfqjY/DLBpMuI66PIdwVmtfyRVek/img.jpg?width=3024&amp;amp;height=3024&amp;amp;face=0_0_3024_3024');&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;[MySQL] 자연어 검색 (FULLTEXT)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 검색 쿼리를 작성할 때, 기본적을 와일드카드를 사용하여 LIKE 검색을 이용하는 것을 떠올릴 것이다. 이 방법은 가장 기본적이며 사용하기 쉬워 어렵지 않게 적용을 할 수 있으나, 데&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jangbageum.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5.2 복합 인덱스 설계 순서&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;복합 인덱스의 컬럼 순서는 성능에 직접적인 영향을 준다. &lt;b&gt;카디널리티가 높은 컬럼 &amp;rarr; 낮은 컬럼 순서로, 등호(=) 조건 &amp;rarr; 범위 조건 순서&lt;/b&gt;로 배치한다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777609480827&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- member_id(=)로 필터 후 create_at(범위)으로 정렬하는 쿼리가 많다면
CREATE INDEX play_history_member_id_create_at_IDX ON play_history (member_id, create_at);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. 하지 말아야 할 것들&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모델링에서 &quot;무엇을 해야 하는가&quot;만큼 중요한 것이 &lt;b&gt;&quot;무엇을 하지 말아야 하는가&quot;&lt;/b&gt;&amp;nbsp;다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6.1 Stored Procedure / Trigger / Event Scheduler&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;쓰지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이유는 많지만 핵심은 두 가지다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. &lt;b&gt;비즈니스 로직이 분산된다.&lt;/b&gt; 애플리케이션 코드와 DB에 로직이 나뉘어 있으면, 버그를 추적할 때 두 곳을 모두 봐야 한다. 코드 리뷰, 테스트, 배포 파이프라인에도 포함되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. &lt;b&gt;MySQL SP는 성능상 이점이 없다.&lt;/b&gt; Oracle과 달리 MySQL은 SP(저장 프로시저)를 캐시 하지 않는다.(Oracle은 저장 프로시저 실행 시 실행 계획이나 관련 객체를 메모리에 유지(캐싱)하는 방식이 강한 반면, MySQL은 상대적으로 그런 캐싱 방식이 다르다) 호출할 때마다 매번 컴파일되고, SQL 파싱 오버헤드도 누적된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비즈니스 로직은 &lt;b&gt;애플리케이션 레이어에서 관리&lt;/b&gt;하는 것이 디버깅, 테스트, 배포 모든 면에서 유리하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6.2 물리적 FK 제약조건&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;논리적 모델에서 FK를 그리는 것은 중요하다. 하지만 &lt;b&gt;물리적 모델(실제 DDL)에 FK 제약조건을 거는 것은 권장하지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;쓰기 성능 저하&lt;/b&gt;: FK 제약조건은 INSERT/UPDATE/DELETE마다 참조 무결성 검사를 수행한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;잠금 경합&lt;/b&gt;: 참조 테이블에 추가적인 잠금이 발생하여 동시성이 떨어진다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;마이그레이션 복잡도 증가&lt;/b&gt;: 스키마 변경 시 FK 순서를 고려해야 하며, 대량 데이터 마이그레이션이 번거로워진다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;분산 환경 비호환&lt;/b&gt;: 샤딩이나 분산 DB 환경에서는 노드 간 FK 제약을 유지할 수 없다 참조 무결성은 &lt;b&gt;애플리케이션 레벨에서 보장하고,&lt;/b&gt; DB에는 인덱스만 걸어두는 것이 현실적인 선택이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777611190372&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- FK 제약 대신 인덱스만 생성
CREATE TABLE playlist_content (
	playlist_content_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    playlist_id BIGINT UNSIGNED NOT NULL,
    content_id BIGINT UNSIGNED NOT NULL,
    sort_no INT UNSIGNED NOT NULL DEFAULT 0,
    create_at DATETIME NOT NULL,
    -- FK 제약 없이 인덱스만
    INDEX playlist_content_playlist_id_IDX (playlist_id),
    INDEX playlist_content_content_id_IDX (content_id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;6.3 JSON 컬럼 남용&lt;/b&gt; &lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MySQL 5.7부터 JSON 타입을 지원하지만, 남용하면 관계형 DB의 장점을 스스로 포기하는 꼴이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JSON 컬럼의 주요 문제점:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;인덱싱 제한&lt;/b&gt;: JSON 내부 데이터를 인덱싱 하려면 가상 컬럼(generated column)을 별도로 만들어야 한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;스키마 검증 없음&lt;/b&gt;: 어떤 구조의 JSON이든 삽입 가능하므로 데이터 일관성이 깨진다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;JOIN 불가&lt;/b&gt;: JSON 내부 데이터로 다른 테이블과 JOIN 하는 것이 사실상 불가능하다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;파싱 오버헤드&lt;/b&gt;: 조회할 때마다 JSON 문자열을 파싱해야 한다 JSON이 적합한 경우는 &lt;b&gt;스키마가 자주 변경되는 메타데이터&lt;/b&gt;나 &lt;b&gt;구조가 유동적인 설정값&lt;/b&gt; 정도다. 그 외의 정형 데이터는 테이블을 쪼개서 정규화하는 것이 관계형 DB를 제대로 쓰는 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6.4 COUNT(*)를 검증 로직에 태우는 것&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Oracle과 달리 MySQL(InnoDB)은 `row_num`을 별도로 저장하지 않는다. &lt;b&gt;`COUNT(*)`는 매번 전체 행을 스캔&lt;/b&gt;한다. 데이터가 수백만 건이 넘는 테이블에서 `COUNT(*)`를 검증 로직에 태우면 심각한 성능 저하가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;이 회원의 시청 기록이 존재하는가?&quot;를 확인하고 싶다면:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777614417345&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 느리다 (X)
SELECT COUNT(*) FROM play_history WHERE member_id = 456;

-- 빠르다 (O)
SELECT EXISTS(SELECT 1 FROM play_history WHERE member_id = 456);

-- 또는 LIMIT 활용
SELECT 1 FROM play_history WHERE member_id = 456 LIMIT 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;7. 모범 사례 예시&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지금까지의 규칙을 종합하여, 동영상 스트리밍 플랫폼 도메인의 핵심 테이블을 설계해 보겠다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777614538227&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 회원 기본 정보
CREATE TABLE member (
    member_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(255) NOT NULL,
    nickname VARCHAR(50) NOT NULL,
    phone VARCHAR(20) NOT NULL,
    is_active TINYINT(1) NOT NULL DEFAULT 1,
    create_at DATETIME NOT NULL,
    update_at DATETIME NOT NULL,
    CONSTRAINT member_email_UIDX UNIQUE (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 장르
CREATE TABLE genre (
    genre_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    parent_id INT UNSIGNED NULL,
    name VARCHAR(50) NOT NULL,
    depth TINYINT UNSIGNED NOT NULL DEFAULT 1,
    sort_no INT UNSIGNED NOT NULL DEFAULT 0,
    is_display TINYINT(1) NOT NULL DEFAULT 1,
    create_at DATETIME NOT NULL,
    update_at DATETIME NOT NULL,
    INDEX genre_parent_id_IDX (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 콘텐츠 기본 정보
CREATE TABLE content (
    content_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    genre_id INT UNSIGNED NOT NULL,
    title VARCHAR(200) NOT NULL,
    release_date DATE NOT NULL,
    is_adult TINYINT(1) NOT NULL DEFAULT 0,
    is_display TINYINT(1) NOT NULL DEFAULT 1,
    synopsis TEXT NULL,
    create_at DATETIME NOT NULL,
    update_at DATETIME NOT NULL,
    INDEX content_genre_id_IDX (genre_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 구독 기본 정보
CREATE TABLE subscription (
    subscription_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    member_id BIGINT UNSIGNED NOT NULL,
    subscription_no VARCHAR(20) NOT NULL COMMENT '구독번호',
    subscription_fee DECIMAL(10,2) UNSIGNED NOT NULL,
    payment_method VARCHAR(20) NOT NULL,
    subscription_status VARCHAR(20) NOT NULL DEFAULT 'active',
    start_date DATE NOT NULL,
    expire_date DATE NOT NULL,
    create_at DATETIME NOT NULL,
    update_at DATETIME NOT NULL,
    INDEX subscription_member_id_IDX (member_id),
    CONSTRAINT subscription_subscription_no_UIDX UNIQUE (subscription_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 몇 가지 포인트를 짚어보면:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- `user` 대신 `member`를 선택한 이유는 `user`가 MySQL 예약어에 가까워 쿼리 작성 시 백틱이 필요할 수 있기 때문이다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- FK 제약조건 대신 일반 인덱스를 사용했다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- `genre_id`는 장르 수가 제한적이므로 `INT`로 충분하다. `content_id`는 대량 데이터가 예상되므로 `BIGINT`를 사용했다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 모든 테이블에 `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`를 명시했다&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;font-family: 'Noto Serif KR';&quot;&gt;DB 모델링 컨벤션은 결국 &lt;b&gt;&quot;미래의 나와 팀원을 위한 약속&quot;이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지금 당장은 아무렇게나 만들어도 동작한다. 하지만 서비스가 커지고, 데이터가 쌓이고, 팀원이 바뀌었을 때, 그때 가서 후회하는 것은 언제나 설계 단계에서의 타협이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 글에서 다룬 내용을 한 줄씩 요약하면 이렇다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;명명 규칙&lt;/b&gt;: snake_case, 단수형, 타입별 접미사 통일, 예약어 금지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;데이터 타입&lt;/b&gt;: 가장 작은 타입, DECIMAL로 금액, DATETIME으로 시간&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;PK&lt;/b&gt;: BIGINT UNSIGNED + Auto Increment, 복합키 PK 금지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;인덱스&lt;/b&gt;: 필요한 만큼만, 복합 인덱스는 순서가 핵심&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;안티패턴&lt;/b&gt;: SP/Trigger 금지, 물리 FK 제거, JSON 남용 금지, COUNT(*) 검증 금지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컨벤션이 &quot;귀찮은 제약&quot;이 아니라 &quot;편안한 기본값&quot;이 되려면, 규칙의 &lt;b&gt;이유&lt;/b&gt;를 팀원 모두가 이해해야 한다. 왜 이렇게 하는지 납득이 되어야 자발적으로 지키게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt; &lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- [MySQL 8.0 Reference Manual &amp;mdash; Data Types](&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/data-types.html)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/data-types.html)&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- [MySQL 8.0 Reference Manual &amp;mdash; CREATE INDEX Statement](&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/create-index.html)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/create-index.html)&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;[Use&amp;nbsp;The&amp;nbsp;Index,&amp;nbsp;Luke&amp;nbsp;&amp;mdash;&amp;nbsp;A&amp;nbsp;Guide&amp;nbsp;to&amp;nbsp;Database&amp;nbsp;Performance](&lt;a href=&quot;https://use-the-index-luke.com/)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://use-the-index-luke.com/)&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;[Percona&amp;nbsp;&amp;mdash;&amp;nbsp;InnoDB&amp;nbsp;Primary&amp;nbsp;Key&amp;nbsp;versus&amp;nbsp;Secondary&amp;nbsp;Index](&lt;a href=&quot;https://www.percona.com/blog/)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.percona.com/blog/)&lt;/a&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;nbsp;[MySQL&amp;nbsp;Reserved&amp;nbsp;Words](&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/keywords.html)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/keywords.html)&lt;/a&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/DataBase</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/109</guid>
      <comments>https://jangbageum.tistory.com/109#entry109comment</comments>
      <pubDate>Fri, 1 May 2026 15:32:54 +0900</pubDate>
    </item>
    <item>
      <title>나만의? RESTful API 설계 원칙</title>
      <link>https://jangbageum.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;AI가 생활화되면서 개발에 있어 더욱 중요해진 것이 있다. 바로 &lt;b&gt;설계&lt;/b&gt;라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;AI 에이전트들은 각자의 컨텍스트를 가지고 각자의 요구사항을 해결하는 데 집중한다. 하지만 우리는 하나의 프로젝트 전체 요구사항을 단일 AI 컨텍스트에 맡기지 않는다. AI의 컨텍스트도 거대한 요구사항을 감당하지 못한다. 이 과정에서 서비스의 일관성을 유지하고 유지보수성을 높이기 위해, 인간이 할 수 있는 '설계'의 역할은 더욱 중요해졌다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;특히 외부로 제공되는 API는 한 번 정해지면 서비스가 종료되기 전까지 사라질 수 없다. 업데이트를 진행할 때도 하위 호환성은 반드시 고려해야 한다. 이런 면에서 API 설계는 대충 넘어갈 일이 아니라, 충분한 시간을 들여 고민해야 하는 영역이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;이전에 &lt;a href=&quot;https://jangbageum.tistory.com/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;API 디자인 패턴&lt;/a&gt;이라는 글을 쓴 적이 있다. 그때는 여러 매체에서 배운 일반적인 원칙들을 정리한 느낌이었다면, 이번 글은 조금 다르다. 실제로 마이크로서비스 기반의 플랫폼을 설계하고 운영하면서 체득한, 말 그대로 **&quot;나만의 기준&quot;**을 정리해보려 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;여기서 말하는 원칙들은 교과서적인 REST 규칙의 나열이 아니다. &quot;왜 이렇게 해야 하는가&quot;에 대한 나의 생각을 담은 것이다. 누군가에게는 당연하게 느껴질 수도 있고, 누군가에게는 동의하기 어려울 수도 있다. 하지만 여기서 내가 말하고 싶은 것은 &quot;왜&quot;를 기록하는 것이다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 이야기하고 싶은 것들&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 글에서 다루는 내용은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0. 설계의 기조&lt;br /&gt;1. URI 네이밍&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. HTTP Method 활용&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. HTTP 상태 코드&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. API 버전 관리&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. 에러 응답 구조&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. 응답 필드 네이밍&lt;/span&gt;&lt;br /&gt;7. 필드 네이밍 접두사 접미사&lt;br /&gt;8. 기타 헷갈리는 부분들&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 설계의 기조&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구체적인 원칙에 들어가기 전에, 내가 API를 설계할 때 항상 염두에 두는 두 가지 마음가짐이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;명세만으로 이해할 수 있어야 한다.&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;좋은 API란 무엇일까? 나는 &lt;b&gt;명세만 보고도 동작을 예측할 수 있는 API&lt;/b&gt;라고 생각한다. 내부 구현을 몰라도, 코드를 읽지 않아도, API 문서만으로 &quot;이 요청을 보내면 이런 응답이 오겠구나&quot;를 알 수 있어야 한다. API를 처음 접하는 개발자가 별도의 설명 없이도 통합할 수 있다면, 그것이 잘 설계된 API다. 반대로 &quot;이건 문서에 없는데 실제로 해보니까 이렇게 동작하더라&quot;라는 말이 나온다면, 설계에 대한 고민이 부족했거나 서비스에 대한 애정이 부족했다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;수정에는 닫혀 있고, 확장에는 열려 있어야 한다.&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;언젠가 후임자가 이 API를 유지보수하게 될 것이다. 그때 기존 클라이언트를 깨뜨리지 않으면서도 새로운 기능을 추가할 수 있는 구조여야 한다. 필드를 추가하는 것은 괜찮지만, 기존 필드의 의미를 바꾸거나 삭제하는 것은 위험하다. 새로운 엔드포인트를 만드는 것은 괜찮지만, 기존 엔드포인트의 동작을 변경하는 것은 피해야 한다. 처음 설계할 때 이 점을 염두에 두면, 나중에 하위호완성에 대한 고통받을 일이 훨씬 줄어들 것이라 생각된다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 원칙들&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 1. URI 네이밍은 꼭 정하고 꼭 지키자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;URI는 HTTP Method와 함께 API를 대표하는 이름이다. 사용자가 API 문서를 열었을 때 가장 먼저 보는 것이 URI다. 그만큼 URI 네이밍은 API의 첫인상을 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;URI 네이밍에 자주 마주치는 고민들이 있다. &quot;/sync-store&quot;처럼 동사를 슬 것인가. &quot;/stores&quot;처럼 명사를 쓸 것인가? 단수형 &quot;/store&quot;와 복수형 &quot;/stores&quot;중 어떤 것이 맞는가? 경로가 길어지면 어떻게 표현할 것인가? 이런 질문들에 대해 명확한 기준이 없으면 API마다 스타일이 달라지고 사용하는 클라이언트에게 혼란을 줄 수밖에 없다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;규칙&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스는 복수형 명사로 표현한다&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&quot;/store&quot;와 &quot;/stores&quot; 중 무엇이 맞을까? 보통 &lt;b&gt;복수형&lt;/b&gt;을 사용하는 것이 일반적이다. 이유는 복잡하지 않다.&quot;/stores&quot;는 매장 컬렉션을 의미하고, &quot;/stores/:store_id&quot;는 그 컬렉션에서 특정 매장 하나&quot;를 의미한다. 단수형을 쓰면 &quot;/store&quot;가 하나의 매장을 가리키는 것처럼 보이는데, 실제로는 목록을 반환한다. 의미와 동작이 일치하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;경로는 kebab-case, 경로 매개변수는 snake_case&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;URI 경로에는 &lt;b&gt;kebab-case&lt;/b&gt;를, 경로 매개변수에는 &lt;b&gt;snake_case&lt;/b&gt;를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;GET&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &lt;/b&gt;/v1/order-items/:order_item_id&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;왜 이렇게 나눴을까? 경로(order-items)는 URL의 일부로, 대소문자를 구분하지 않는 환경도 있고 언더스코어가 링크에서 밑줄과 겹쳐 보이기도 한다. 반면 경로 매개변수(order_item_id)는 코드에서 변수로 바인딩되는 값이다. 대부분의 언어에서 변수명에 하이픈은 사용하지 않음으로, snake_case가 자연스럽다 생각된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770472011547&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 변수 명에 하이픈을 사용하지 못하는 언어도 많고 부자연스럽다.
@Param(&quot;order-item-id&quot;) orderItemId
@Param(&quot;order_item_id&quot;) orderItemId&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;e.g.&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;font-family: 'Noto Serif KR';&quot;&gt;Good&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/b&gt;&lt;/span&gt;/v1/users&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp; 사용자 목록 조회&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/b&gt;/v1/users/:user_id&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp; 특정 사용자 조회 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;POST&lt;/span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/b&gt;/v1/products&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp; 상품 생성 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/b&gt;/v1/orders/:order_id/items&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp; 특정 주문의 상품 목록 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; /v1/products/:product_id/reviews&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp; 특정 상품의 리뷰 목록 &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Bad &lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; /v1/getUserList&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp; 동사 사용 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; /v1/user/:userId &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt; 단수형 + camelCase &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;POST&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt; /v1/product/create &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt; 경로에 행위 포함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컬렉션과 리소스의 관계 표현&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;리소스 간에 소유관계가 있을 때는 &quot;/&quot;로 계층을 표현한다. &quot;유저의 대출 목록&quot;, &quot;특정 버전의 기능 값&quot;처럼 &lt;b&gt;A 없이는 B가 존재할 수 없는 관계&lt;/b&gt;일 때 유용하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; /v1/orders/:order_id/items &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt; 주문의 상품 목록 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; /v1/orders/:order_id/items/:item_id &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt; 주문 내 특정 상품 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;GET&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt; /v1/products/:product_id/reviews/:review_id &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt; 상품의 특정 리뷰&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;다만 계층이 3단계를 넘어가면 URI가 매우 길어진다. 이럴 때는 무리하게 계층을 표현하기보다, 독립적인 리소스 엔드포인트로 분리하는 것이 나을 수 있다. URI의 가독성과 리소스 간 관계 표현 사이에서 타협을 보는 것이 매우 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;액션을 포함한 URI&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;RESTful API 원칙에서는 URI에 동사를 넣지 않는 것이 정석이다. 하지만 이는 조금만 API를 설계를 하다 보면 현실적으로 어렵다고 느낄 때가 많다. &quot;재시작&quot;, &quot;롤백&quot;처럼 어려운 행위는 어떻게 설계를 해야 하는 것일까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;이 경우에는 &quot;/actions/cancel&quot;, &quot;/actions/refund&quot;처럼 &lt;b&gt;액션 네임스페이스&lt;/b&gt;를 두고 동사를 허용하는 방식으로 고민을 하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;POST&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; /v1/orders/:order_id/actions/cancel &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; 주문 취소 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;POST&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; /v1/orders/:order_id/actions/refund &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; 주문 환불 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;POST&lt;/span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; /v1/payments/:payment_id/actions/retry &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; 결제 재시도&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;핵심읜 &lt;b&gt;예외를 허용하되, 예외의 패턴은 일관되게 유지&lt;/b&gt;하는 것이다. &quot;동사를 쓸 때는 항상 '/control/' 또는 '/actions/' 아래에 둔다&quot;는 규칙만 지키면, 예외도 예측 가능해진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style8&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 2 HTTP Method로 행위를 표현하되, 예외를 인정한다.&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;원칙 1 중 액션을 포함한 URI 내용과 일부 겹치지만 이 부분에서는 행위를 표현하는 Method에 초점을 두고 이야기한다. RESTful API에서 HTTP Method는 곡 행위이다. 이건 누구나 아는 이야기이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;하지만 실무에서는 CRUD로 깔끔하게 떨어지지 않는 행위들이 정말 많다. &quot;이 상품을 품절 처리해 줘&quot;, &quot;주문을 취소해라&quot; 같은 요청을 어떤 Method로 표현해야 하는가? 이 질문에 대한 기준이 필요하다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;규칙&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 106px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; text-align: left; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Method&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1473%; text-align: left; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.6356%; text-align: left; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상태 코드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #409d00; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;GET&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1473%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 조회 (읽기 전용)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.6356%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;200 OK&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #006dd7; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;POST&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1473%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 생성, 또는 &lt;b&gt;프로세스 트리거&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.6356%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;201 Created / 202 Accepted&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #99cefa; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;PUT&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1473%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 전체 교체&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.6356%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;200 OK&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #f89009; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;PATCH&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1473%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 부분 업데이트, 또는 &lt;b&gt;리소스 속성 변경&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.6356%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;200 OK&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.217%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #ef5369; font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;DELETE&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.1473%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 삭제&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.6356%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;200 OK / 204 No Content&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;PATCH와 POST, 어떻게 구분할까?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;헷갈리는 것이 &quot;상품 품절 처리&quot;와 &quot;주문 취소&quot;의 차이다. 둘 다 상태를 바꾸는 행위인데, 왜 하나는 PATCH고 하나는 POST일까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;나는 단순히 리소스의 속성을 변경하냐 와 부수 효과가 따르냐로 판단을 하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&amp;nbsp;PATCH는 리소스의 속성을 변경&lt;/b&gt;하는 것이다. &quot;is_sold_out&quot;을 &quot;true&quot;로 바꾸는 것은 상품이라는 리소스의 한 필드를 수정하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770474544752&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 상품 품절 처리: 리소스의 속성 변경 &amp;rarr; PATCH
@Patch(&quot;/products/:product_id&quot;)
updateProduct(@Body() body: { is_sold_out: boolean }) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;b&gt;POST는 프로세스를 트리거&lt;/b&gt;하는 것이다. &quot;주문 취소&quot;는 단순히 주문의 &quot;status&quot;를 &quot;cancelled&quot;로 바꾸는 게 아니다. (물론 복잡하지 않은 비즈니스를 갖는 서비스는 아닐 수 있다.) 재고 복원, 결제 취소, 알림 발송 등 여러 부수 효과가 따른다. 이런 경우는 프로세스 트리거로 보고 POST를 사용한다. (URI 네이밍은 원칙 1에서 다룬 &quot;/actions/&quot; 패턴을 따른다.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770474776516&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 주문 취소: 프로세스 트리거 &amp;rarr; POST
@Post(&quot;/orders/:order_id/actions/cancel&quot;)
cancelOrder() { ... }&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;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예외를 허용하게 된다면,,,&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;위에서도 이야기하였지만 실무에서는 이론대로 되지 않는 순간이 너무 많다. 예를 들어, 복합 조건으로 리소스를 조회해야 하는데 조건이 너무 많아 Query String이 지나치게 길어지는 경우 등이 그렇다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770474891138&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 원칙대로라면 GET이 맞지만, 검색 조건이 많아 POST로 처리
@Post(&quot;/search&quot;)
@HttpCode(HttpStatus.OK)
searchProducts(@Body() body: ProductSearchRequest) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;이런 경우에는 주석으로 &quot;왜 원칙을 벗어났는가&quot;를 반드시 남겨둔다. 원칙을 깨는 것 자체가 문제가 아니라, &lt;b&gt;왜 깼는지를 기록하지 않아 아무도 모르게 하는 것이 문제&lt;/b&gt;라 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 3. HTTP 상태 코드는 의미에 맞게 사용한다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;에러 응답 body에 &quot;status_code&quot; 필드를 넣는 API를 종종 본다. 하지만 HTTP 표준 자체가 이미 상태 코드를 응답 헤더에 포함하고 있다. body에 중복해서 넣을 필요가 없다. &lt;b&gt;상태 코드 자체를 정확하게 사용하는 것&lt;/b&gt;이 더 중요하다. 모든 에러에 400 혹은 500을 내려주거나, 성공 응답에 무조건 200만 쓰는 것은 HTTP를 제대로 활용하지 못하는 것이라 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%; height: 265px;&quot; border=&quot;1&quot; data-source-line=&quot;184&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상태 코드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;사용 시점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;186&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;200 OK&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요청 성공&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;조회, 수정, 삭제 성공&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;187&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;201 Created&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 생성 성공&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;POST로 새 리소스 생성 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;188&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;202 Accepted&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요청 접수됨 (처리 미완료)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비동기 작업 요청 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;189&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;204 No Content&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;성공, 응답 본문 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삭제 성공 등 본문이 불필요한 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;190&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;400 Bad Request&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;잘못된 요청&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;입력값 검증 실패&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;191&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;401 Unauthorized&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인증 실패&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;토큰 없음, 만료 등&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;192&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;403 Forbidden&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인가 실패&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;권한 부족&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;193&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;404 Not Found&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리소스 없음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;존재하지 않는 리소스 접근&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;194&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;409 Conflict&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;충돌&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미 존재하는 리소스 생성 시도 등&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;195&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;422 Unprocessable Entity&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요청은 올바르나 처리 불가&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비즈니스 규칙 위반&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;196&quot;&gt;
&lt;td style=&quot;height: 21px; width: 25.5814%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;500 Internal Server Error&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 33.3721%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서버 내부 오류&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 40.8139%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예상치 못한 예외&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;참고&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Status&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN Web Docs - HTTP response status codes&lt;/a&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;흔한 실수&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;400과 422의 구분이 대표적이다.&lt;b&gt; 400은 요청 자체가 잘못된 경우&lt;/b&gt;(필수 필드 누락, 타입 불일치 등)이고, &lt;b&gt;422는 요청 형식은 올바르지만 비즈니스 규칙에 의해 처리할 수 없는 경우&lt;/b&gt;(이미 탈퇴한 회원의 주문 요청 등)이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;또 하나 자주 헷갈리는 것은 401과 403이다. &lt;b&gt;401은 &quot;너 누구냐&quot;&lt;/b&gt;(인증 실패)이고, &lt;b&gt;403은 &quot;너인 것은 아는데 넌 안돼&quot;&lt;/b&gt;(인가 실패)다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 4. API 버전 관리는 처음부터 한다.&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&quot;나중에&quot;, &quot;나중에&quot;,&amp;nbsp; &quot;나중에&quot;,,,라는 말을 정말 많이 하기도 하고 들어봤을 것이다. 결론부터 말하면,&lt;b&gt; 나중은 절대 오지 않았다&lt;/b&gt;. 이미 버전 없이 배포된 API에 버전을 붙이는 작업은 생각보다 훨씬 고통스럽다. 기존 클라이언트를 모두 마이그레이션해야 하고 URI를 변경한다는 것 자체가 하위호완성을 박살 내는 행위이다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;규칙&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;경로 기반으로 버전을 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770476397877&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// NestJS 설정
app.enableVersioning({
  type: VersioningType.URI,
  prefix: &quot;v&quot;,
});

// Controller에서 버전 지정
@Controller({ path: &quot;/products&quot;, version: &quot;1&quot; })
export class ProductsController { ... }

// 결과 경로: /v1/products&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NestJS 프레임워크의 경우 Controller 데코레이터에서 &quot;version&quot; 속성으로 간단하게 관리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여러 버전의 공존&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;서비스가 성장하면 v1과 v2가 동시에 살아있어야 하는 시점이 반드시 오는 것 같다. 지금 만든 API는 영원하지만 또 모두가 영원히 사용하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770476544736&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apps/
├── controllers/
│   └── products/
│       ├── v1/
│       │   └── products.v1.controller.ts    # 기존 API
│       └── v2/
│           └── products.v2.controller.ts    # 개선된 API&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;컨트롤러 레이어 개발 관점에서 중요한 것은 v1코드를 v2에 복붙 하는 것이 아니라. &lt;b&gt;변경이 필요한 부분만 v2에 오버라이드 하는 구조&lt;/b&gt;를 만드는 것이 유지보수에서 유리한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Header 기반 버전 관리 (e.g. Accept: application/vnd.api+v1+json)가 좀 더 &quot;RESTful&quot;하다는 의견도 있는 것 같다. 하지만 경험상 URI 기반이 훨씬 직관적이고, Trace 모니터링 시, 확인도 빠르고 로드밸런서에서 경로 기반 라우팅도 가능하기에 실용 측면에서는 이점이 많은 듯하다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 5. 에러 응답은 일관된 구조를 갖는다.&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;20개가 넘는 마이크로서비스가 각각 다른 형태로 에러를 내려주면 어떻게 될까? API Gateway에서 에러를 통합 처리하는 것이 불가능해진다. 클라이언트 개발하는 사람은 각 이용하는 서비스마다 에러를 처리하는 로직을 여러 개로 작성해야 한다. 이는 지속 가능하지 못한다고 본다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;규칙&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770477178898&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;code&quot;: &quot;ERR_SHOP_ORDER_0400&quot;
  &quot;message&quot;: &quot;Invalid request.&quot;,
  &quot;detail&quot;: &quot;product_id must be a non-empty string&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-source-line=&quot;270&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;필드&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-source-line=&quot;272&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;code&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;에러 식별. 클라이언트가 분기 처리할 때 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-source-line=&quot;273&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;message&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;무엇이 잘못됐는지. 개발자가 로그에서 빠르게 파악&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-source-line=&quot;274&quot;&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;detail&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 잘못됐는지. 디버깅에 필요한 구체적인 정보&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;code&quot;가 있으면 클라이언트가 메시지 문자열에 의존하지 않고 에러를 처리할 수 있다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;과연 더 많은 에러 정보가 필요하지 않을까?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;에러 응답의 모범사례를 검색해 보면 훨씬 많은 필드를 권장하는 예시들을 종종 볼 수 있다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;e.g.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770477721111&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출처 - https://blog.postman.com/best-practices-for-api-error-handling/

{
  &quot;status&quot;: &quot;error&quot;,
  &quot;status_code&quot;: 404,
  &quot;error&quot;: {
    &quot;code&quot;: &quot;RESOURCE_NOT_FOUND&quot;,
    &quot;message&quot;: &quot;요청한 리소스를 찾을 수 없습니다.&quot;,
    &quot;detail&quot;: &quot;ID가 '12345'인 사용자는 존재하지 않습니다.&quot;,
    &quot;timestamp&quot;: &quot;2023-12-08T12:30:45Z&quot;,
    &quot;path&quot;: &quot;/api/v1/users/12345&quot;,
    &quot;suggestion&quot;: &quot;사용자 ID가 올바른지 확인하세요.&quot;
  },
  &quot;request_id&quot;: &quot;a1b2c3d4-e5f6-7890&quot;,
  &quot;documentation_url&quot;: &quot;https://api.example.com/docs/errors&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;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;status, status_code&lt;/b&gt;: HTTP 응답 헤더에 이미 있다. 중복이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;timestamp, path, request_id&lt;/b&gt;: 디버깅에 유용하지만, 서버 로깅 시스템에서 관리할 영역이다. 굳이 클라이언트에게 내려줄 필요가 없다. request_id가 필요하다면 응답 헤더로 내려주면 된다. 그리고 비동기 응답을 다루는 API라면 필요할 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;documentation_url, suggestion&lt;/b&gt;: 친절해 보이지만, 클라이언트가 이 URL을 자동으로 열어줄까 의문이다. 결국 개발자가 읽는 것이고, 개발자는 이미 문서를 읽고 잘 찾지 않을까 생각된다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;에러 코드 체계&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;에러 코드는 각 조직마다 정하기 나름이다. 내가 운영하는 서비스의 경우 마이크로 서비스를 운영하고 있기 때문에 &lt;b&gt;어떤 서비스로부터 전파된 에러인지&lt;/b&gt; 알기 위해서 개발자가 확인할 수 있는 프로젝트, 서비스 코드를 함께 넣어 코드를 만들고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f5f5f5; color: #333333; text-align: left;&quot; data-source-line=&quot;316&quot; data-info=&quot;{data-source-line=&amp;quot;316&amp;quot;}&quot; data-role=&quot;codeBlock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;ERR_{프로젝트}_{서비스}_{코드번호}

ERR_SHOP_ORDER_0400  &amp;rarr; Shop / Order Service / 400번 에러
ERR_SHOP_USER_0001   &amp;rarr; Shop / User Service / 커스텀 에러 0001
ERR_SHOP_PAY_0422    &amp;rarr; Shop / Payment Service / 422번 에러&lt;/code&gt;&lt;/pre&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;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 6. 응답 필드 네이밍은 snake_case로 통일한다&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;JavaScript/TypeScript 생태계에서 변수명은 camelCase가 표준이다. 그래서인지 Google, Microsoft 같은 JS/TS 생태계 중심의 API는 camelCase를 권장하기도 한다. 반면 GitHub, Stripe, AWS처럼 다양한 언어의 클라이언트를 지원하는 API들은 snake_case를 채택하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;어떤 것을 선택하든 결국 클라이언트는 직렬화/역직렬화 라이브러리를 사용하게 된다. API가 snake_case를 쓰든 camelCase를 쓰든, 클라이언트 측 언어의 린팅 규칙에 맞게 변환하는 과정은 어차피 필요하다. 그 과정에서 데이터 타입 변환(문자열 &amp;rarr; 날짜 등)도 함께 처리되므로, API 측 네이밍이 클라이언트 언어와 다르다는 것은 실질적인 문제가 되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;그렇다면 선택의 기준은 &lt;b&gt;가독성&lt;/b&gt;과 &lt;b&gt;일관성&lt;/b&gt;이다. order_item_id는 orderItemId 보다 단어 경계가 명확해서 읽기 쉽다. 그리고 어떤 표기법을 택하느냐보다, 한번 정한 규칙을 모든 API에서 일관되게 지키는 것이 훨씬 중요하다. 나는 이러한 이유로 snake_case를 선택했다.&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;규칙&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;API 응답&lt;/b&gt; 필드: snake_case&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;내부 도메인 모델&lt;/b&gt;: camelCase (나는 주로 TypeScript를 사용하기 때문)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 변환은 &lt;b&gt;Mapper&lt;/b&gt;에서 담당&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;background-color: #f5f5f5; color: #333333; text-align: left;&quot; data-source-line=&quot;342&quot; data-info=&quot;typescript {data-source-line=&amp;quot;342&amp;quot;}&quot; data-role=&quot;codeBlock&quot;&gt;&lt;code&gt;// API 응답 (snake_case)
{
  &quot;product_id&quot;: &quot;prod_abc123&quot;,
  &quot;product_name&quot;: &quot;무선 키보드&quot;,
  &quot;is_sold_out&quot;: false,
  &quot;created_at&quot;: &quot;2024-01-15T10:30:45.123Z&quot;
}

// 내부 도메인 모델 (camelCase)
interface ProductEntity {
  productId: string;
  productName: string;
  isSoldOut: boolean;
  createdAt: Date;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내부적으로 camelCase를 쓰고 외부로는 snake_case를 내려주는 구조는 변환 레이어(Mapper)가 필요하다. 그러나 앞에서 이야기한 듯 제대로 된 클라이언트라면 어차피 직렬화/역직렬화를 거친다 생각하기 때문에 이 비용은 크게 신경 안 써도 될 것이라 생각한다.&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 7. 필드 네이밍에 접두사와 접미사를 명확히 사용하자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;필드 이름만 보고도 타입과 용도를 예측할 수 있으면 너무 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&quot;status&quot;라는 필드가 있다고 하자. 이게 문자열인가, 숫자인가, Boolean인가 한 번에 알기가 쉽지 않다. created는 생성자인가, 생성 여부인가, 생성 시각인가 명확히 알 수가 없다. 필드 이름이 모호하면 매번 문서를 확인해야 하는 수고가 생긴다. 접두사와 접미사를 일관되게 사용하면, 필드 이름 자체만으로 행위를 파악하는데 많은 시간을 단축할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Boolean: &quot;is_&quot;, &quot;has_&quot;, &quot;can_&quot;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Boolean 필드는 &quot;예/아니요&quot;로 답할 수 있는 질문 형태로 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-source-line=&quot;378&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;접두사&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;380&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;is_&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;~인가? (상태)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;is_sold_out,&amp;nbsp;is_verified,&amp;nbsp;is_active&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;381&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;has_&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;~을 가지고 있는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;has_coupon,&amp;nbsp;has_children,&amp;nbsp;has_permission&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-source-line=&quot;382&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;can_&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;~할 수 있는가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;can_cancel,&amp;nbsp;can_edit,&amp;nbsp;can_refund&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;sold_out&quot;만 쓰면 품절 상태인지, 품절 시각인지, 품절된 수량인지 모호하다. &quot;is_sold_out&quot;은 Boolean 임이 분명하게 느껴진다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;날짜/시간: &quot;_at&quot;, &quot;_date&quot;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-source-line=&quot;396&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.4651%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;접두사&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 59.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-source-line=&quot;398&quot;&gt;
&lt;td style=&quot;width: 15.4651%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;_at&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;특정 시점 (시각 포함)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 59.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;created_at,&amp;nbsp;ordered_at&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-source-line=&quot;399&quot;&gt;
&lt;td style=&quot;width: 15.4651%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;_date&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;날짜만&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 59.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;birth_date,&amp;nbsp;expiry_date&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;created&quot;만 쓰면 생성자를 뜻하는지, 생성 시각을 뜻하는지 알 수 없다. &quot;created_at&quot;은 시각임이 분명하다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;식별자: &quot;id&quot;, &quot;_code&quot;, &quot;_number&quot;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;식별자는 성격에 따라 접미사를 구분한다. 이 부분이 설계할 때 자주 헷갈리는 영역이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;접미사성격예시&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-line=&quot;407&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 12.907%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;접미사&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 38.9535%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;성격&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 47.907%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예시&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;409&quot;&gt;
&lt;td style=&quot;height: 21px; width: 12.907%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 38.9535%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;해당 리소스 자체의 식별자&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 47.907%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;id&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.907%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;{resource}_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.9535%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;다른 리소스를 참조할 때&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.907%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;user_id, order_id, product_id&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;410&quot;&gt;
&lt;td style=&quot;height: 21px; width: 12.907%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;_code&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 38.9535%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;짧고 의미 있는 코드. 사람이 읽고 기억할 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 47.907%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;category_code,&amp;nbsp;country_code&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;411&quot;&gt;
&lt;td style=&quot;height: 21px; width: 12.907%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;_number&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 38.9535%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;순차적으로 부여되는 번호. 사용자에게 노출됨&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 47.907%;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;order_number,&amp;nbsp;invoice_number&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&quot;id&quot;는 접미사가 아니다.&lt;/b&gt; 리소스 자체의 식별자는 그냥 id로 표현한다. &quot;user_id&quot;나 &quot;order_id&quot;처럼 &lt;b&gt;접미사 형태로 쓰는 것은 다른 리소스를 참조할 때뿐&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770553163578&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// User 리소스 응답
{
  &quot;id&quot;: &quot;550e8400-e29b-41d4-a716-446655440000&quot;,
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;
}

// Order 리소스 응답 - 다른 리소스(User)를 참조할 때 user_id 사용
{
  &quot;id&quot;: &quot;order_abc123&quot;,
  &quot;user_id&quot;: &quot;550e8400-e29b-41d4-a716-446655440000&quot;,
  &quot;order_number&quot;: &quot;ORD-2024-00001234&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;User 응답에서 user_id가 아닌 id를 쓰는 이유는 간단하다. 이미 User 리소스의 응답이라는 맥락 안에 있기 때문에, user_라는 접두사는 중복이다. 반면 Order 응답에서 주문한 사용자를 참조할 때는 user_id라고 명시해야 어떤 리소스의 식별자인지 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;order_number는 고객에게 보여주는 주문번호다. 고객센터에 문의할 때 &quot;ORD-2024-00001234 주문 확인해 주세요&quot;라고 말한다. id와는 용도가 완전히 다르다. _code는 enum처럼 미리 정의된 값 중 하나를 선택하는 경우에 주로 쓴다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;category_code: &quot;ELECTRONICS&quot;, status_code: &quot;PENDING&quot; 같은 식이다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;배역/목록: 복수형&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컬렉션은 복수형으로 표현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770553251678&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;products&quot;: [...],
  &quot;reviews&quot;: [...],
  &quot;order_items&quot;: [...]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원칙 8. 자주 헷갈리는 설계들&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;설계하다 보면 사소하지만 매번 고민되는 부분들이 있다. 정답이 있다기보다는 한번 정도 일관되게 정하면 되는 부분들이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시간의 포맷: ISO 8601 vs Unix Timestamp&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시간을 어떤 포맷으로 내려주는 것이 좋을까? 처음에는 ISO 8601 표준의 문자열을 자주 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770553405285&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;created_at&quot;: &quot;2026-02-01T10:30:45.123Z&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;사람이 읽기 좋고, 표준이라는 장점이 있다. 하지만 클라이언트에서 이 &lt;b&gt;문자열을 파싱 하는 과정에서 문제가 종종 생겼다.&lt;/b&gt; 로컬 장비의 시간대 설정에 따라 파싱 결과가 달라지는 것이다. 같은 &quot;2026-02-01T 10:30:45.123Z&quot;를 파싱해도, 어떤 환경에서는 UTC로, 어떤 환경에서는 로컬 시간대로 해석하여 시간이 어긋나는 버그가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러하여 문자열 파싱 오류를 최소화할 수 있는 &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;b&gt;Unix Timestamp(밀리초)&lt;/b&gt;를 사용해 보았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f5f5f5; color: #333333; text-align: left;&quot; data-source-line=&quot;471&quot; data-info=&quot;json {data-source-line=&amp;quot;471&amp;quot;}&quot; data-role=&quot;codeBlock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;created_at&quot;: 1705314645123
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Timestamp는 시간대와 무관한 절댓값이다. 1970년 1월 1일 UTC 기준으로 몇 밀리초가 지났는지를 나타내는 숫자일 뿐이다. 파싱 과정에서 시간대로 인한 오류가 발생할 여지가 없다. 클라이언트는 이 값을 받아서 자신의 시간대에 맞게 변환해서 보여주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사람이 읽기 어렵다는 단점이 있지만, 어차피 API 응답을 사람이 직업 읽는 경우는 디버깅할 때 분이니 안정성이 더 중요하다 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;과거분사 vs 동사 원형&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;create_at&quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;인가, &quot;&lt;/span&gt;created_at&quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;인가?&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770555213001&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ &quot;create_at&quot;: 1705314645123 }
{ &quot;created_at&quot;: 1705314645123 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;과거분사&lt;/b&gt;(created_at)를 사용하는 것이 일반적으로 보인다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #333333; text-align: start;&quot; data-source-line=&quot;492&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-source-line=&quot;492&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;created_at: &quot;생성된 시각&quot; - 이미 일어난 사건의 시점&lt;/span&gt;&lt;/li&gt;
&lt;li data-source-line=&quot;493&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;create_at: &quot;생성하다 시각&quot; - 문법적으로 어색함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동사 원형을 쓰면 행위를 나타내는 것처럼 보인다. 반면 과거분사는 &quot;~된 시각&quot;이라는 완료된 상태를 명확히 표현한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만 예정된 시점을 나타낼 때는 현재형이 자연스럽니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f5f5f5; color: #333333; text-align: left;&quot; data-source-line=&quot;499&quot; data-info=&quot;json {data-source-line=&amp;quot;499&amp;quot;}&quot; data-role=&quot;codeBlock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;created_at&quot;: 1705314645123,
  &quot;expires_at&quot;: 1735689599000
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;expires_at&quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;은 &quot;만료되는 시각&quot;으로, 아직 일어나지 않은 미래 시점이다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;단수/복수 형태가 같은 단어&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; goods&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;news&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;,&amp;nbsp;&lt;/span&gt;species&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;처럼 단수와 복수가 같은 단어는 어떻게 할까? 필드명만 보고는 배열인지 단일 객체인지 구분하기 곤란하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;이런 경우 간단하게&lt;b&gt; &quot;_list&quot; 접미사&lt;/b&gt;를 사용하여 사용하거나, 혹은 아예 &lt;b&gt;다른 단어로 대체&lt;/b&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f5f5f5; color: #333333; text-align: left;&quot; data-source-line=&quot;514&quot; data-info=&quot;json {data-source-line=&amp;quot;514&amp;quot;}&quot; data-role=&quot;codeBlock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 방법 1: _list 접미사
{
  &quot;goods_list&quot;: [...]
}

// 방법 2: 다른 단어로 대체
{
  &quot;items&quot;: [...],
  &quot;products&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;goods&quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;를 굳이 고집할 이유가 없다면, &quot;&lt;/span&gt;products&quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;나 &quot;&lt;/span&gt;items&quot;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;처럼 복수형이 명확한 단어를 선택하는 게 낫다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;내가 위와 같이 정한 내용은 모두 절대적인 정답이 아니다. 내가 실무를 하면서 외부 서비스 연동을 위해 다양한 타사의 API 정의서를 본 경험가 20개가 넘는 마이크로서비스를 설계하고 운영하면서, 반복적으로 부딪힘과 함께 고민한 내용들을 정리한 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;처음에는 하나하나 정하기 번거롭고 무슨 이런 걸 정해두나 과해 보일 수도 있지만 이 규칙들이 시간이 지날수록 유지보수에 큰 도움을 주고 생산성에 큰 영향을 준다. 반면 성능과 안정성 사이에서 절충점을 찾아야 하는 순간도 분명히 온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;내가 여기서 강조하고 싶은 것은 &quot;내가 이렇게 정했다!&quot;가 아니다. 결국 API 설계에서 가장 중요한 것은 &lt;b&gt;일관성&lt;/b&gt;과 &lt;b&gt;명확성&lt;/b&gt;의 중요성이다. 어떤 규칙을 따르느냐보다, 한번 정한 규칙을 일관되게 지키는 것이 훨씬 중요하다. 그래서 이런 원칙들을 기록해 두는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;그리고 앞에서 이야기했던 기조를 다시 한번 강조하고 싶다. &lt;b&gt;명세만으로 이해할 수 있어야 하고, 수정에는 닫혀 있되 확장에는 열려 있어야 한다.&lt;/b&gt; 지금 당장은 번거로워 보여도, 이 두 가지를 지키려고 노력하면 나중에 고통받을 일이 현저히 줄어들 것이라 생각된다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend/개발 방법론 &amp;amp; 디자인 패턴</category>
      <category>AI</category>
      <category>API</category>
      <category>HTTP</category>
      <category>nestjs</category>
      <category>restfulapi</category>
      <category>백엔드</category>
      <category>설계</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/107</guid>
      <comments>https://jangbageum.tistory.com/107#entry107comment</comments>
      <pubDate>Sat, 7 Feb 2026 14:11:38 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code를 슬기롭게 사용하기 (Claude Code 잘 사용하는 방법)</title>
      <link>https://jangbageum.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;요즘 &lt;b&gt;AI 코딩 에이전트&lt;/b&gt; 없이 개발하는 사람은 거의 없을 것이다. 이 영역의 문화도 빠르게 변하고 있다. 기획부터 개발, QA까지 단순히 AI 에이전트에게 명령하는 수준을 넘어서, 유기적인 에이전트 팀을 구성하여 하나의 제품을 만들어내는 방법론들도 다양하게 등장하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;우리 조직에서도 Claude를 도입하여 실무에서 적극적으로 사용하고 있다. 개인적으로 Claude Code의 능력은 정말 잘하는 수준의 주니어를 뛰어넘을 정도라 생각하고, 생산력도 무시무시할 정도로 높아진 것이 체감된다. (Claude Code를 도입하면서 오히려 내 일이 늘어나는 아이러니한 상황이 벌어지고 있다...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;그런데 결국 모두가 AI 코딩 에이전트를 이용하여 개발하고 있고, 어느 정도 생산력이 상향 평준화된 상황에서 한 가지 의문이 생겼다. 과연 여기서 더 높은 생산력과 더 좋은 품질의 제품을 만들어낼 수 있을까? 멀리 볼 것도 없이 우리 팀 각 인원들이 AI 에이전트를 어떻게 쓰고 있는지만 봐도 생산력과 품질의 미묘한 차이가 왜 나는지는 어렵지 않게 발견할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나는 Claude Code를 메인으로 사용하면서 느낀 점이 하나 있다. 처음에는 &quot;이거 만들어줘&quot;라고 던지면 알아서 뚝딱 잘 만드는 것 같았고, 모든 작업을 이렇게 시작하고 마무리하면 되겠다 생각했다. 그러나 최종 결과물을 보면 만족하지 못하는 경우가 많았다. 구체적으로는 다음과 같은 문제들이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;결국 내가 Claude에게 명령한 작업들은 프로젝트 전반적으로 넓은 시야를 가지고 수행한 것이 아니었다. 마치 요청한 일만 해치우듯 작업한 결과물로밖에 보이지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;이 문제는 도구의 문제가 아니다. &lt;b&gt;사용 방식의 문제&lt;/b&gt;였다. Claude Code를 제대로 쓰려면 코더가 아니라 &lt;b&gt;매니저처럼 일해야 한다&lt;/b&gt;. 설계하고, 계획을 세우고, 지휘하는 것이다. 이 글에서는 내가 Claude Code를 실무에서 사용하면서 어떤 마음가짐으로 접근하고 있는지, 그리고 어떻게 하면 더 효과적으로 활용할 수 있는지 정리해 보려 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CjT7L3wBN8-1280.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlzwl0/dJMcabC3Cgs/E944fU6S26wBHnh83V9qek/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlzwl0/dJMcabC3Cgs/E944fU6S26wBHnh83V9qek/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlzwl0/dJMcabC3Cgs/E944fU6S26wBHnh83V9qek/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdlzwl0%2FdJMcabC3Cgs%2FE944fU6S26wBHnh83V9qek%2Fimg.webp&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;754&quot; data-filename=&quot;CjT7L3wBN8-1280.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 핵심 마인드셋: 코더가 아닌 매니저가 되자&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Claude Code의 창시자 Boris Chemy는 다음과 같은 말을 했다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;더 좋은 프롬프트가 아니라, 더 나은 환경을 만들어라.&quot;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;처음에는 프롬프트를 어떻게 잘 써야 하나에 집중했다. 하지만 사용할수록 느낀 것은 프롬프트 하나를 정교하게 다듬고 명령하는 것보다, &lt;b&gt;에이전트가 잘 일할 수 있는 구조를 만드는 것&lt;/b&gt;이 훨씬 효과적이라는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 65.1163%; height: 108px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 31.1865%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3335%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;전환 후&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 31.1865%; height: 22px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코더 마인드&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3335%; height: 22px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;매니저 마인드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 31.1865%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;한 번에 하나씩 지시&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3335%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;로드맥 주고 병렬 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 31.1865%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결과물만 확인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3335%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스펙으로 방향 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 31.1865%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;에이전트가 질문&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3335%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;에이전트가 자율 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 31.1865%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;더 좋은 프롬프트 작성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 32.3335%; height: 16px; text-align: left;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자동 검증 + 규칙 누적 환경 구축&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 전환의 핵심은 두 가지다. 첫째, 에이전트에게 &lt;b&gt;충분한 맥락과 명확한 계획&lt;/b&gt;을 미리 제공하여 불필요한 질문 없이 자율적으로 작업하게 만드는 것, 둘째, 자동 검증과 피드백 루프, 팀 규칙이 &lt;b&gt;축적되는 구조&lt;/b&gt;를 만드는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 핵심 전략&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.1 작업은 Plan 모드로 시작하고, 스펙으로 실행하라&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;스펙 기반 반복 1회 = 스펙 없는 반복 8회&quot;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;코딩부터 시작하지 말고 &lt;b&gt;설계부터 작성한다&lt;/b&gt;. Boris Cherny도 &quot;좋은 계획이 정말 중요하다&quot;라고 강조하며, 대부분의 세션을 &lt;b&gt;Plan 모드&lt;/b&gt;로 시작한다고 한다. Plan 모드에서 만족스러운 계획을 만든 뒤 auto-accept edits로 전환하면, 대부분 한 번에 작업이 끝난다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;에이전트는 항상 제한적인 컨텍스트 공간을 가지고 있다. 이 컨텍스트는 지워지거나 압축되면 에이전트는 환각 증세(hallucination)를 가지게 되고 중간에 맛이 갈 수도 있다. 그렇게 때문에 여기서 한 단계 더 나아가, Claude Code와 세운 계획은 문서로 남기고 재사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. Claude와 대화하면 계획된 스펙은 &quot;spec.md&quot; 파일로 작성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 구현 단계를 체크 리스트로 정리하여 &quot;plan.md&quot; 파일로 작성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. Claude Code에게 전달: &quot;@plan.md 파일을 읽고 Task 1부터 실행해 줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;산출물은 두 가지다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;spec.md&lt;/b&gt; - 전체 아키텍처, 모듈 인터페이스, 데이터 모델&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;plan.md&lt;/b&gt; - 구현 테스크 체크리스트, 의존성 관계&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;설계 없이 바로 구현에 들어가면 에이전트가 방향을 잡지 못하고, 결국 여러 번 반복하게 된다. 스펙 문서를 먼저 작성하면 에이전트가 한 번에 원하는 결과를 만들어낼 확률이 크게 올라간다. 또한 아키텍처, 데이터 모델링 등 마음에 들지 않은 부분은 문서로 확인하며 바로 수정 요청 후 계획에 반영할 수 있도록 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;설계를 요청할 떄는 4단계의 요소를 따르면 품질이 올라가는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. Context (맥락) - &quot;왜 만드는가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. Requirements (요구사항) - &quot;무성르 해야 하는가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. Constrainsts (제약조건) - &quot;무엇을 지켜야 하는가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. Deliverables (산출물) - &quot;무엇을 받고 싶은가?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AI 에이전트를 이용한 개발이 아닌 평소 개발 전에 작성하는 문서들의 요소들과 크게 다르지는 않다. 이 부분을 고민할 때는 &lt;b&gt;정통적인 개발 공학 지식&lt;/b&gt;의 중요성을 알게 되는 부분이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프롬프트 예시:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769761138115&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;E-commerce API를 설계해줘.
- 사용자 인증
- 상품 관리
- 주문 처리
- 결제 연동

각 모듈의 책임, 인터페이스, 데이터 흐름을 spec.md로 작성해.
먼저 내가 놓친 부분이나 명확히 해야 할 점이 있으면 질문해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;마지막에 &quot;&lt;b&gt;질문해 줘&lt;/b&gt;&quot;를 붙이는 것은 아주 중요하다 생각된다. 에이전트가 빠진 부분을 짚어주면서 설계의 품질을 올릴 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.2 작업은 병렬로 실행&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;한 번에 하나의 작업만 시키면 매우 비효율 적이다. 터미널을 어려 개 띄우면 Claude Code의 세션을 여러 개 이용할 수 있다. 이렇게 &lt;b&gt;병렬로 띄워진 세션들에서 병렬로 독립적인 작업을 요청&lt;/b&gt;할 수 있는 이유는 앞 전 단계에서 &lt;b&gt;spec.md 파일과 plan.md를 명확히 작성했기 때문&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 터미널 1 - Task 1(AuthModule)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 터미널 2 - Task 2(ProductModule)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 터미널 3 - Task 3(CartModule)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실행 방법:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 터미널을 여러 개 열고 각 터미널에서 Claude Code 세션 시작&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 각 세션에 서로 다른 모듈 구현 요청&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 시스템 알림으로 입력이 필요한 시점을 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 모든 세션이 완료되면 통합 진행&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;독립적인 모듈 단위로 작업을 쪼개야 병렬 처리가 가능하다. 이를 위해 설계 단계에서 모듈 간 의존성을 최소화하고, 인터페이스를 먼저 정의해 두는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.3 컨텍스트의 관리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Claude를 똑똑하게 유지하려면 &lt;b&gt;기억을 관리&lt;/b&gt;해야 한다. 이전 작업의 잔상이 환각을 유발할 수 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메모리 관리 명령어:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;/clear&lt;/b&gt; - 기억 완전 초기화&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;/compact&lt;/b&gt; - 기억 요약 압축&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;작업이 하나 끝날 때마다 &lt;b&gt;/clear&lt;/b&gt; 또는 &lt;b&gt;/compact&lt;/b&gt;로 컨텍스트를 정리하는 습관을 들이자. 컨텍스트가 누적되면 에이전트의 판단력이 흐려진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.4 CLAUDE.md로 규칙을 누적&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컨텍스트&amp;nbsp;관리의&amp;nbsp;핵심&amp;nbsp;도구가&amp;nbsp;바로&amp;nbsp;&lt;b&gt;CLAUDE.md&lt;/b&gt;다.&amp;nbsp;프로젝트&amp;nbsp;루트에&amp;nbsp;CLAUDE.md&amp;nbsp;파일을&amp;nbsp;만들면&amp;nbsp;&lt;b&gt;모든&amp;nbsp;세션에&amp;nbsp;자동&amp;nbsp;적용&lt;/b&gt;된다.&amp;nbsp;이&amp;nbsp;파일에&amp;nbsp;프로젝트&amp;nbsp;규칙,&amp;nbsp;코딩&amp;nbsp;스타일,&amp;nbsp;금지&amp;nbsp;사항을&amp;nbsp;정의해&amp;nbsp;두면&amp;nbsp;매&amp;nbsp;세션마다&amp;nbsp;반복&amp;nbsp;설명할&amp;nbsp;필요가&amp;nbsp;없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예시:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769761788708&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Project Rules

## 코딩 스타일
- TypeScript strict mode 사용
- 파일당 200줄 이하 유지
- 인지 복잡도는 15 이하
- 순환 복잡도는 10 이하
- SOLID 원칙 준수

## 네이밍 규칙
- 파일명: kebab-case
- 클래스: PascalCase
- 상수명: UPPERCASE_SNAKE_CASE
- 변수명: camelCase

## 금지 사항
- any 타입 금지
- console.log 커밋 금지
- 타입 단언 금지

## 자주 하는 실수 (학습됨)
- DTO 검증 데코레이터 누락
- 트랜잭션 처리 누락&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;핵심은 &lt;b&gt;실수가 발생할 때마다 규칙을 추가&lt;/b&gt;하는 것이다. 한 번 추가한 규칙은 이후 모든 세션에 적용되므로 같은 실수가 반복되지 않는다. 이는 &lt;b&gt;복리 효과&lt;/b&gt;를 갖는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;CLAUDE.md 파일은 잘 정의하면 &lt;b&gt;팀 내에서 공유하여 공통적으로 사용도 가능&lt;/b&gt;하다. 또한 프로젝트 단위로도 파일 세팅이 가능해서 PR 리뷰를 올릴 때, @claude 태그를 활용하는 방법 등을 사용해서 학습한 내용을 CLAUDE.md에 추가하도록 요청하면, 코드 리뷰가 곧 AI 에이전트를 이용한 운영 시스템의 강화 루틴이 될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.5 검증 루프는 필수!!!&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Claude가 자신이 작성한 코드는 스스로 평가하게 만들어야 한다. 특히 오래 걸리는 작업일수록 결과 리스크가 크기 때문에, 검증 루프를 붙여 품질을 안정화하는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;b&gt;TDD 방식&lt;/b&gt;으로 진행할 수 있도록 미리 학습시킨다.(CLAUDE.md 에 추가) 또 작업 후 모델이 &lt;b&gt;스스로 틀렸다는 것을 알 수 있도록 하는 환경&lt;/b&gt;을 제공하는 것이 좋다. 테스트 코드를 실행하거나. UI 작업을 한다면 브라우저에서 테스트를 직접 수행하게 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp; 또 중요한 것은 코드의 검토는 같은 컨텍스트에서 진행하는 것은 좋지 않다. &lt;b&gt;에이전트는 자신이 작성한 코드는 옳다고 착각&lt;/b&gt;을 계속 가지고 있다. 그러하여 제 3자의 검토 방식으로 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 별도 세션에서 코드 리뷰어 역할 부여: &quot;코드 리뷰어 역할로 검토해 줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 별도 세션에서 버그 탐지 역할 부여: &quot;버그 헌터 역할로 취약점 찾아줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;혹은 간단한 수정이었다면 앞에서 말한 &quot;/clear&quot;와 같이 &lt;b&gt;컨텍스트 정리를 진행 후 리펙터링 계획&lt;/b&gt;을 물어봐도 좋은 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심은 구현하는 세션과 검토하는 세션을 분리하면 더 객관적인 피드백을 받은 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6 자동화 적극 활용&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;결국 반복되는 작업은 꼭 있다. Claude Code에는 반복 작업을 줄여주는 자동화 기능들이 있다. 이것을 활용하면 생산성이 훨씬 빨라질 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6.1 슬래시 커맨드 (SKILS)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;반복되는 프롬프트를 매번 치지 않고 슬래시 커맨드로 만들어 재사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;프로젝트 루트 혹은 개인 개발 환경의 루트 경로의 &lt;b&gt;.claude/commands/&lt;/b&gt;&amp;nbsp;경로에 마크다운 파일을 만들면, Claude Code가 자동으로 인식해서 &quot;&lt;b&gt;/파일명&lt;/b&gt;&quot;으로 호출할 수 있게 된다. git을 이용한다면 팀 전체가 동일한 커맨드를 공유 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;설정 예시:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.claude/commands/review.md&amp;nbsp;파일을&amp;nbsp;만든다:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769763373670&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
name: review
description: 코드 리뷰를 수행한다
allowed-tools: Read, Grep, Glob
---

코드 리뷰어 역할로 $ARGUMENTS를 리뷰해줘.

- 보안 취약점 확인
- 에러 처리 누락 확인
- 네이밍 일관성 점검
- 심각도별로 분류해서 리포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;호출 시:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769763400712&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/review src/service/payment&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;YAML 프론트매터 주요 옵션:&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;213&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;필드&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;설명&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;215&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;name&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;슬래시 커맨드 이름 (필수)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;216&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;description&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;용도 설명. Claude가 자동 호출 판단에 활용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;217&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;allowed-tools&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;해당 커맨드 실행 시 허용할 도구 목록&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;218&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;model&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;이 커맨드에 사용할 모델 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;219&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;context: fork&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;별도 서브에이전트 컨텍스트에서 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;220&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;disable-model-invocation: true&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;사용자만 호출 가능 (Claude 자동 호출 방지)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동적 컨텍스트 주입도 가능하다. &quot;&lt;b&gt;!`명령어`&lt;/b&gt;&quot; 문법으로 셀 명령 결과를 커멘드 내용에 삽입할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769763714204&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
name: pr-summary
context: fork
allowed-tools: Bash(gh *)
---
PR diff: !`gh pr diff`
변경 파일: !`gh pr diff --name-only`

위 PR의 변경사항을 요약해줘.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;상세 참고 - &lt;a href=&quot;https://code.claude.com/docs/en/skills&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Code - Skils&lt;/a&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6.2 서브에이전트&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;자주 쓰는 작업 패턴을 서브에이전트로 만들어 자동화할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;슬래시 커맨드가 &quot;명령어&quot;라면, 서브에이전트는 &lt;b&gt;&quot;독립된 컨텍스트 윈도우에서 특정 역할을 수행하는 전문 에이전트&quot;&lt;/b&gt;*라 얘기한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;서브에이전트는 메인 대화와 분리된 독립적인 컨텍스트 윈도우에서 실행된다. 각 서브에이전트는 자체 시스템 프롬프트, 사용 가능한 도구 목록, 원한 설정을 가진다. Claude가 작업 중 서브에이전트의 목적에 맞는 태스크를 인식하면 자동으로 위임하거나, 사용자가 직접 호출할 수도 있다. 작업 완료 후 결과만 메인 대화에 반환되므로 메인 컨텍스트가 오염되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이도 슬래시 커멘드와 비슷하게 &quot;&lt;b&gt;. claude/agents/&lt;/b&gt;&quot; 폴더 경로에 정의 가능하며 프로젝트 루트, 개인 개발 환경 루트에 모두 세팅 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용 방법:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;/agents&quot; 명령어로 가이드된 생성 인터페이스를 사용하거나, 직접 마크다운 파일을 작성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.claude/agents/code-simplifier.md:&lt;/p&gt;
&lt;pre id=&quot;code_1769763959458&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
name: code-simplifier
description: 구현 완료된 코드를 더 단순하고 읽기 쉽게 정리한다
tools: Read, Edit, Glob, Grep
model: sonnet
---

당신은 코드 단순화 전문가입니다.
주어진 코드를 분석하고 다음 기준으로 개선하세요:

- 중복 코드 제거
- 함수 분리 (20줄 초과 시)
- 불필요한 추상화 제거
- 네이밍 개선
- 기능 변경 없이 구조만 개선&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;.claude/agents/verify-app.md&lt;span style=&quot;color: #abb2bf; text-align: start;&quot;&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769763979516&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;---
name: verify-app
description: E2E 테스트를 실행하고 결과를 분석한다
tools: Read, Bash, Glob, Grep
model: sonnet
---

당신은 QA 엔지니어입니다.
다음 순서로 애플리케이션을 검증하세요:

1. 단위 테스트 실행 및 결과 분석
2. E2E 테스트 실행 및 결과 분석
3. 실패한 테스트의 원인 분석
4. 수정 방안 제시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주요 설정 옵션:&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-line=&quot;294&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;필드&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;설명&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;296&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;tools&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;허용할 도구 목록 (화이트리스트)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;297&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;disallowedTools&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;차단할 도구 목록 (블랙리스트)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;298&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;model&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;사용할 모델 (sonnet, opus, haiku)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-line=&quot;299&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;permissionMode&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;권한 모드 (default, acceptEdits, plan 등)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 슬래시 커맨드와 서브에이전트의 차이는 뭔가?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;슬래시 커맨드는 &quot;&lt;b&gt;반복 프롬프트를 재사용&lt;/b&gt;&quot; 하는 것이고 서브에이전트는 &quot;&lt;b&gt;전문 역할 수행&lt;/b&gt;&quot;이라는 명확한 목적의 차이가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-line=&quot;303&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt; 구분&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;슬래시 커맨드&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;서브에이전트&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;305&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;컨텍스트&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;메인 대화 공유 (또는&amp;nbsp;context: fork로 분리)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;항상 독립 컨텍스트&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;306&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;역할&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;반복 프롬프트 재사용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;전문 역할 수행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;307&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;도구 제어&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;allowed-tools로 제한 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;tools/disallowedTools로 세밀 제어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-line=&quot;308&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;호출&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;/커맨드명으로 직접 호출&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;Claude가 자동 위임 또는 사용자 요청&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;상세 참고 - &lt;a href=&quot;https://code.claude.com/docs/en/sub-agents&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Code - Sub-agents&lt;/a&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6.3 Hooks&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Hook은 Claude Code의 라이프사이클 특정 시점에 자동 실행되는 셸 명령이다. Claude가 도구를 사용한 뒤 자동으로 실행되는 스크립트를 설정할 수 있다. 가장 대표적으로 사용할 수 있는 사례로는 코드 작성 후 자동 포맷팅이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;PostToolUse는 도구 실행이 성공적으로 완료된 직후에 발동된다. Hook은 JSON 형태의 입력을 stdin으로 받고, 종료 코드로 결과를 제어한다 (0: 성공, 2: 액션 차단).&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;지원되는 Hook 이벤트:&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;color: #abb2bf; text-align: start; border-collapse: collapse; width: 100%; height: 186px;&quot; border=&quot;1&quot; data-line=&quot;321&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt; 이벤트발동&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 16px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt; 시점 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;323&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;PreToolUse&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;도구 실행&amp;nbsp;전&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;324&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;PostToolUse&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;도구 실행 성공&amp;nbsp;후&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;325&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;PostToolUseFailure&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;도구 실행 실패 후&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;326&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;UserPromptSubmit&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;사용자 프롬프트 제출 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;327&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;SessionStart&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;세션 시작 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;328&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;Stop&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;에이전트 응답 완료 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-line=&quot;329&quot;&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;Notification&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px;&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Serif KR';&quot;&gt;시스템 알림 시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나도 이 것은 사용해보지 않았으니 사용 법은 아래 상세 참고에서 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;상세 참고 - &lt;a href=&quot;https://code.claude.com/docs/en/hooks&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Code - Hooks&lt;/a&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6.4 권한 관리&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Claude Code는 도구별 권한 시스템을 제공한다. 매번 승인을 누르는 것은 좋은 방법은 아니지만 모든 승인을 뛰어넘는 것은 매우 좋은 방법은 아니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Claude Code의 도구는 세 가지 접근 수준으로 나뉜다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;읽기 전용 도구&lt;/b&gt; (파일 읽기, grep 등): 승인 불필요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;Bash 명령&lt;/b&gt;: 승인 필요, &quot;다시 묻지 않기&quot; 설정 시 프로젝트 디렉터리 단위로 기억&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;파일 수정 도구&lt;/b&gt;: 승인 필요, 세션 단위로 기억&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&amp;nbsp;&quot;--dangerously-skip-permissions&quot; 플래그의 사용은 지양해야 한다.&lt;/b&gt; 모든 권한 확인을 건너뛰는 이 플래그는 편리할 수도 있지만 Claude가 실행하는 셀 명령에 대한 안전장치가 완전히 제거된다. &quot;/permissions&quot;로 자주 쓰는 안전한 명령만 선별적으로 혀용 하는 것이 올바른 접근이다. (rm 혹은 git reset 등 위험한 명령어를 Claude가 맘대로 쓰게 하고 싶다면 사용해도 좋다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;권한 규칙은 allow, ask, deny 세 카테고리로 관리되며, &lt;b&gt;deny -&amp;gt; ask -&amp;gt; allow 순서로 평가&lt;/b&gt;&amp;nbsp;된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;세팅은 &quot;/permissions&quot; 명령어로 설정 가능하고 &quot;. claude/settings.json&quot;에 직접 작성도 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;상세 참고 - &lt;a href=&quot;https://code.claude.com/docs/en/security&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Code - IAM, Claude Code - Settings&lt;/a&gt;=&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.6.5 MCP 활용&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MCP(Model Context Protocol)은 정말 많이 들어봤을 것 같다. 여기에서는 상세하게는 다루지 않겠지만 현재 내가 유용하게 사용하는 MCP들의 목록과 유의해야 할 사항은 차후에 따로 기록하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 에이전트의 역할 정의와 워크플로우&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Claude Code를 효과적으로 사용하려면 &lt;b&gt;역할을 분리&lt;/b&gt;해서 사용하는 것이 좋다. 마치 실제 개발팀처럼 각 역할에 맞는 프롬프트를 던지는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769846844206&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  Human - 지휘 (사용자)
    │
    ▼
Architect (설계자) - spec.md 작성, 아키텍처 결정
    │
    ▼
Planner (계획자) - prompt_plan.md 작성, 태스크 분할
    │
    ├── Generator1 (모듈 A, 터미널1)
    ├── Generator2 (모듈 B, 터미널2)
    └── Generator3 (모듈 C, 터미널3)
    │
    ▼
Integrator (통합자) - 모듈 조립, 통합 테스트
    │
    ▼
Healer (치료자) - 실패 테스트 수정, 버그 패치
    │
    ▼
Reviewer (검토자) - 코드 품질, 보안 취약점, 성능 이슈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;각 역할별로 다음과 같이 프롬프트를 시작할 수 있을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Architect&lt;/b&gt; - &quot;~를 설계해 줘. spec.md 작성해.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Planner&lt;/b&gt; - &quot;@spec.md 읽고 plan.md 작성해 줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Generator&lt;/b&gt; - &quot;@spec.md의 [모듈명] 세션만 읽고 구현해 줘&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Integrator&lt;/b&gt; - &quot;Integrator 역할 수행. 모듈들을 통합하고 테스트해&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Healer&lt;/b&gt; - &quot;Healer 역할 수행해. [에러 목록] 수정해&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Reviewer -&amp;nbsp; &quot;코드 리뷰어 역할 수행해. 전체 코드 리뷰해&quot;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3.1 설계와 계획&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Plan 모드에서 충분히 설계한 뒤, &quot;spec.md&quot;와 &quot;plan.md&quot;를 확정한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769847547557&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@spec.md 읽고 병렬 구현 계획 세워줘.
- 독립적으로 진행 가능한 모듈 식별
- 각 모듈별 Generator 프롬프트 생성
- 의존성 있는 작업은 순서 지정
- prompt_plan.md로 정리해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3.2 통합, 수정, 검토&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;통합 단계에서는 `/clear` 후 새로운 컨텍스트로 시작하는 것을 권장한다. 이전 구현 과정의 잡음이 통합 판단에 영향을 줄 수 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 리팩토링은 꼭 진행하자&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Claude Code를 활용한 개발에서 리팩토링은 &lt;b&gt;특정 시점&lt;/b&gt;들 마다 요청하는 것이 효과적이다. 마지막에 한꺼번에 리팩토링을 요청하려고 하면 시간도 오래 걸리고 동작에 문제가 생기거나 다시 스파게티 코드가 될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 Generator 구현이 완료된 후&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 개별 모듈 내부 코드들 정리&lt;/span&gt;&lt;br /&gt;&lt;b&gt;버그 수정 시 구조적 문제 발견&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 문제 영역 국소 리팩토링&lt;/span&gt;&lt;br /&gt;&lt;b&gt;Reviewer 피드백 반영&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 전체 코드 품질 개선&lt;/span&gt;&lt;br /&gt;&lt;b&gt;컨텍스트가 커졌을 때&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 현재 작업 영역 정리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리팩토링 요청 예시:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769848013697&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AuthModule 구현 완료했어. 통합 전에 리팩토링해줘.
- 중복 코드 추출
- 함수/메서드 분리 (20줄 이상이면 분리)
- 프로젝트 일관성 점검
- 순환 복잡도, 인지 복잡도 개선
기능 변경 없이 구조만 개선해.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&amp;nbsp;&quot;기능 변경 없이&quot;&lt;/b&gt;를 명시하면 Claude가 동작을 보존하면서 구조만 개선한다. 또한 프로젝트 일관성을 꼭 지켜달라 요청을 해야지 스파게티 코드가 되는 것을 막을 수 있다. 리팩토링 과정에서 각 모듈이 일관성 없이 각자의 구조를 가지게 되면 차후 코드 수정과 파악에 많은 힘을 쓰게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리펙토링 시 구체적인 기준을 명시하면 일관된 결과를 얻는데 도움이 될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;순환 복잡도 - 10~15 이하&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;함수 길이 - 20~30줄 이하&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;중첩 깊이 - 3~4단계&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;매개변수 개수 - 3~4개 이하&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. 에이전트 친화적 아키텍처&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;위에서 언급한 &quot;병렬 작업&quot;을 가능하게 하려면 에이전트 친화적인 애플리케이션 아키텍처를 만드는 것도 매우 중요한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;에이전트가 코드를 보고 수정하던 혹은 프로젝트 전체 맥락을 보고 모듈을 새로 만들던 각 요소들의 흐름과 역할이 명확해야 더 높은 이해력을 갖고 더 좋은 품질의 코드를 작성하는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;모듈 독립성&lt;/b&gt; -&amp;gt; 병렬 구현 가능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;명확한 인터페이스&lt;/b&gt; -&amp;gt; 미리 설계된 인터페이스만 보고 구현 가능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;작은 모듈 크기&lt;/b&gt; -&amp;gt; 컨텍스트 윈도우 안에서 처리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;단방향 의존성&lt;/b&gt; -&amp;gt; 통합 시 충돌 최소화&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;테스트 용이성&lt;/b&gt; -&amp;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;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;나의 취향이 많이 들어갔지만 추천하는 구조는 &lt;b&gt;&quot;Layerd + Hexagonal (Port &amp;amp; Adapter)&quot; 패턴&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769849139972&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;project-root/
├── CLAUDE.md              &amp;larr; 에이전트 규칙
├── spec.md                &amp;larr; 설계 문서
├── plan.md         &amp;larr; 구현 계획
│
├── src/
│   ├── domain/            &amp;larr; 도메인 엔티티 (의존성 없음)
│   ├── port/              &amp;larr; 추상화 인터페이스
│   ├── adapter/           &amp;larr; Port의 구현체
│   ├── controller/        &amp;larr; API 진입점
│   ├── service/           &amp;larr; 비즈니스 로직
│   ├── framework/         &amp;larr; 프레임워크 설정
│   └── infrastructure/    &amp;larr; 외부 연동 (Client)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;이 구조를 따르게 되면 &lt;b&gt;&quot;domain&quot;과 &quot;port&quot;를 먼저 명확하게 정의&lt;/b&gt;만 하게 된다면 이후에 나머지 레이어를 병렬로 구현이 가능하게 된다. 각 Generator가 &quot;port&quot;인터페이스만 보고 독립적으로 구현하면 되기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. 사용 안티패턴&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;font-family: 'Noto Serif KR';&quot;&gt;- 설계 없이 &quot;만들어줘&quot; X&amp;nbsp; &amp;nbsp; -&amp;gt; Plan 모드로 시작, &quot;spec.md&quot;먼저 작성 후 구현 O&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 한 세션에서 모든 작업 X&amp;nbsp; &amp;nbsp;-&amp;gt; 터미널 분리해서 병렬 처리 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 컨텍스트 계속 누적 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; -&amp;gt; 작업마다 &quot;/clear&quot; 또는 &quot;/compact&quot; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-결과만 확인 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-&amp;gt; 테스트 먼저 작서 (TDD) + 자동 검증 환경 구축 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 에이전트에게 계속 질문받고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt;&amp;nbsp; -&amp;gt; 로드맵(plan.md) 미리 제공 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 반복 프롬프트 매번 입력 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-&amp;gt; 슬래시 커맨드 + 서브에이전트로 자동화 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 마지막에 한 번에 리펙토링 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; Phase 별 완료 시점에 리펙토링 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 같은 실수 반복 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-&amp;gt; &quot;CLAUDE.md&quot;에 규칙 추가, 팀 공유 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;O&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &quot;--dangerously-skip-permission&quot; 사용 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;X&lt;/span&gt; -&amp;gt; &quot;/permissions&quot;로 안전하게 권한 허용 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;O&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Claude Code를 쓰면서 가장 크게 바뀐 것은 &lt;b&gt;&quot;개발에 대한 관점&quot;&lt;/b&gt;인 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;코드를 직접 치는 것에서, 설계하고 지휘하는 것으로 무게중심이 옮겨졌다. 물론 여전히 코드를 읽고 이해하는 능력은 필수다. 에이전트가 만든 결과물을 검증하려면 결국 사람이 판단해야 하고 이 작업에 신간을 많이 할애하기 때문이다. AI가 코드를 만든다고 좋은 구조의 코드가 무엇인지 고민을 멈춰서는 안 되고 오히려 코드 일부분에 대한 컨벤션과 성능에 집중을 하는 것이 아닌 아키텍처에 더 큰 고민을 가져야 하는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리하자면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. &lt;b&gt;Plan 모드로 시작하고, 스펙으로 실행하라&lt;/b&gt;&amp;nbsp;- `spec.md` + `prompt_plan.md` 먼저 작성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. &lt;b&gt;병렬로 실행하라&lt;/b&gt;&amp;nbsp;- 여러 터미널에서 동시에 에이전트 운용&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. &lt;b&gt;컨텍스트를 관리하라&lt;/b&gt;&amp;nbsp;- 작업 완료마다 `/clear` 또는 `/compact`&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. &lt;b&gt;CLAUDE.md로 규칙을 누적하라&lt;/b&gt;&amp;nbsp;- 실수할 때마다 규칙 추가, 팀 공유 (복리 효과)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. &lt;b&gt;검증 루프를 붙여라&lt;/b&gt;&amp;nbsp;- TDD + 자동 검증 환경 + 제3자 검토&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. &lt;b&gt;자동화로 반복을 제거하라&lt;/b&gt;&amp;nbsp;- 슬래시 커맨드, 서브에이전트, Hook, MCP 활용&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;AI 코딩 에이전트는 도구이다. 도구를 잘 쓰려면 도구의 특성을 이해하고 그에 맞는 워크플로우를 설계해야 한다. 결국 핵심은 더 정교한 프롬프트를 튜닝하는 것이 아닌, 자동 검증과 피드백 루프, 팀 규칙이 축적되는 &lt;b&gt;환경&lt;/b&gt;을 만드는 것 같다. &quot;코드 짜줘&quot;가 아닌 &quot;설계도 보고 이 모듈 구현해 줘&quot;로 지시 방식을 바꾸는 것만으로도 결과물과 품질이 확연히 달라질 것이라고 확신하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;그리고 정말 마지막으로 Claude와 같이 LLM 서비스와 연계하여 다양한 방법론을 접목한 애플리케이션들이 쏟아져 나오고 있다. 에이전트 오케스트레이션이다 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;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://code.claude.com/docs/en/overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://code.claude.com/docs/en/overview&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://www.gpters.org/dev/post/13-practical-ways-use-EcB0a8yBtR86FDI&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.gpters.org/dev/post/13-practical-ways-use-EcB0a8yBtR86FDI&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend/개발 방법론 &amp;amp; 디자인 패턴</category>
      <category>AI</category>
      <category>AI코딩</category>
      <category>ChatGPT</category>
      <category>Claude</category>
      <category>claude code</category>
      <category>GPT</category>
      <category>개발</category>
      <category>챗지피티</category>
      <category>클로드</category>
      <category>클로드 코드</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/105</guid>
      <comments>https://jangbageum.tistory.com/105#entry105comment</comments>
      <pubDate>Sat, 31 Jan 2026 18:56:23 +0900</pubDate>
    </item>
    <item>
      <title>개발자의 의사결정 기록법 (Architecture Decision Record, ADR)</title>
      <link>https://jangbageum.tistory.com/103</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;운영하는 서비스의 코드 혹은 아키텍처의 설계를 보다 보면 &quot;왜 이렇게 만들었을까?&quot; 하는 정말 순수한 궁금증이 생긴 적이 많았을 것이다. 우리가 현재 운영하는 서비스는 지금의 모습이 되어 열심히 굴러가기까지 수만은 설계의 갈림길에 있었을 것이고 누군가들은 당시 최고의 방향으로 결정하여 서비스를 만들어 나아갔을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러나 이러한 결정은 기록되지 않으면 그 당시 결정을 위한 노력들이 휘발된다. 혹은 실무자의 입에서 입으로 전해오는 구전영하는 서비스의 코드 혹은 아키텍처의 설계를 보다 보면 &quot;왜 이렇게 만들었을까?&quot; 하는 정말 순수한 궁금증이 생긴 적이 많았을 것이다. 우리가 현재 운영하는 서비스는 지금의 모습이 되어 열심히 굴러가기까지 수만은 설계의 갈림길에 있었을 것이고 누군가들은 당시 최고의 방향으로 결정하여 서비스를 만들어 나아갔을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러나 이러한 결정은 기록되지 않으면 그 당시 결정을 위한 노력들이 휘발된다. 혹은 실무자의 입에서 입으로 구전설화처럼 전해 내려오게 된다면 퇴사 앞에서 정보를 보존할 방법은 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;소프트웨어 개발에서는 코드는 버전 관리 시스템을 통해 꼼꼼히 기록이 가능하다. 하지만 정작 그 코드가 왜 그런 형태로 작성되었는지, 어떤 맥락에서 그런 결정이 내려졌는지는 사라지기 쉽다. &lt;b&gt;Architecture Decision Record(ADR)&lt;/b&gt;는 이 문제를 해결하기 위한 실천법이다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR이란?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;소프트웨어 아키텍처와 관련된 중요한 설계 결정을 문서화하는 방법이다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR의 핵심은 단순히 &quot;무엇을 결정했는가&quot;를 기록하는 것이 아니다.&lt;b&gt; &quot;왜 그렇게 결정했는가&quot;, &quot;어떤 대안들이 있는가&quot;, &quot;그 결정으로 인해 어떤 트레이드오프가 발생했는가&quot;&lt;/b&gt;를 함께 남기는 것이다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내가 생각하는 ADR은 &quot;Why&quot;를 정확하게 기록하는 것이라 생각한다. 업무를 위한 업무는 싫다. 어떠한 상황에서 어떤 결정을 &quot;왜&quot; 했는지를 간단명료하게 기록하는 것이라 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;일반적인 기술 문서와 뭐가 다른가?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR을 처음 접하면 &quot;그냥 설계 명세에 쓰면 되는 거 아닌가?&quot;라는 의문이 들 수 있다. 하지만 ADR은 일반적인 기술 문서와 몇 가지 중요한 차이가 있다고 본다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 79.3023%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8158%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;일반 기술 문서&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.8043%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;ARD&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;초점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8158%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;현재 시스템이 어떻게 동작하는가&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.8043%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;왜 이렇게 설계되었는가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;시점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8158%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;현재 상태 스냄샷&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.8043%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;결정 시점의 맥락 보존&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;변경&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8158%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;시스템 변경 시 함께 수정&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.8043%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;불변, 새 ADR로 대체&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 18.6821%; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.8158%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;시스템 이해&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.8043%; text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;결정의 근거 추적&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR은 &lt;b&gt;&quot;불변성&quot;&lt;/b&gt;을 가진다는 점이 특히 주요하다 생각된다. 한 번 작성된 ADR은 수정하지 않는다. 상황이 바뀌어 결정을 변경해야 한다면, 기존 ADR을 &quot;대체됨(Superseded)&quot; 상태로 바꾸고 새로운 ADR을 작성한다. 이렇게 하면 결정의 히스토리가 온전히 보존된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR 작성의 범위&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;보통 소프트웨어 프로젝트 또는 제품에 영향을 미치는 모든 구조적으로 중요한 결정에 대해 ADR을 작성한다.&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;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 구조&amp;nbsp; (e.g. 아키텍처 패턴)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 비기능적 요구 사항&amp;nbsp; ( &lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;e.g.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 보안, 고가용성, 내결합성)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 종속성&amp;nbsp; ( &lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;e.g.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 구성 요소 결함)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 인터페이스&amp;nbsp; ( &lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;e.g.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; API 및 게시된 계약)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 구성 기법&amp;nbsp; ( &lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;e.g.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 라이브러리, 프레임워크, 도구, 프로세스)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ARD의 구조&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ARD의 형식은 사실 정하기 나름이다. 조직마다의 차이점은 있지만, 핵심 구성 요소는 대체로 유사한 듯하다. 가장 널리 사용되는 구조는 다음과 같다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cemi3W/dJMcaaX6Xvt/7gcPkg8VqVHa5sXd1ZmYtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cemi3W/dJMcaaX6Xvt/7gcPkg8VqVHa5sXd1ZmYtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cemi3W/dJMcaaX6Xvt/7gcPkg8VqVHa5sXd1ZmYtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcemi3W%2FdJMcaaX6Xvt%2F7gcPkg8VqVHa5sXd1ZmYtk%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;428&quot; height=&quot;507&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[ 상태 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #666666;&quot;&gt;Status &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR의 현재 상태를 나타낸다. 일반적으로 다음과 같은 상태값을 사용한다:&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;Proposed&lt;/b&gt;: 제안됨. 아직 논의 중이거나 승인 대기 상태&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;Accepted&lt;/b&gt;: 수용됨. 팀에서 이 결정을 채택함&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;Deprecated&lt;/b&gt;: 폐기됨. 더 이상 유효하지 않은 결정&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- &lt;b&gt;Superseded by ADR-XXX&lt;/b&gt;: 대체됨. 새로운 ADR로 대체되었음을 명시&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;상태 관리가 제대로 되지 않으면 ADR의 가치가 크게 떨어진다. 특히 Superseded 상태에서는 반드시 대체하는 ADR 번호를 명시해야 히스토리 추적이 가능하다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[ 문제 정의 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결정이 필요하게 된 배경을 설명한다. 이 섹션이 ADR에서 가장 중요한 부분 중 하나다. 좋은 Context는 다음을 포함한다:&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 현재 시스템의 상태 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 직면한 문제나 요구사항 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 제약 조건 (기술적, 비즈니스적, 조직적) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 관련된 이해관계자 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 시간적 압박이나 우선순위&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[ 결정 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Decision&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;선택한 방향을 명확하게 기술한다. 모호하지 않게, 구체적으로 작성한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[ 대안 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Alternatives Considered&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; ]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;검토했으나 선택하지 않은 옵션들을 기록한다. 이 섹션이 있어야 &quot;왜 다른 방식은 안 되는 거죠?&quot;라는 질문에 답할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[ 결과 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Consequences &lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 결정으로 인해 예상되는 영향을 솔직하게 기록한다. 긍정적인 면만 쓰면 안 된다. 트레이드오프를 명확히 인지하고 있음을 보여줘야 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;작성예시&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;e.g. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메시지 큐 도입&lt;/span&gt;&lt;/i&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;width: 100%; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efu6TO/dJMcahppEgQ/GxkXnXPK8MR4kc4o2cUY40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efu6TO/dJMcahppEgQ/GxkXnXPK8MR4kc4o2cUY40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efu6TO/dJMcahppEgQ/GxkXnXPK8MR4kc4o2cUY40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fefu6TO%2FdJMcahppEgQ%2FGxkXnXPK8MR4kc4o2cUY40%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;710&quot; height=&quot;839&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;839&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCMTM/dJMcafkOwfr/QnJCSk367xN1Kl0EUQd6cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCMTM/dJMcafkOwfr/QnJCSk367xN1Kl0EUQd6cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCMTM/dJMcafkOwfr/QnJCSk367xN1Kl0EUQd6cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCMTM%2FdJMcafkOwfr%2FQnJCSk367xN1Kl0EUQd6cK%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;709&quot; height=&quot;480&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFFGUB/dJMcacO9Cuz/vKMfpIWAKItR67mKa9Chik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFFGUB/dJMcacO9Cuz/vKMfpIWAKItR67mKa9Chik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFFGUB/dJMcacO9Cuz/vKMfpIWAKItR67mKa9Chik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFFGUB%2FdJMcacO9Cuz%2FvKMfpIWAKItR67mKa9Chik%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;704&quot; height=&quot;550&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;어디에 작성하고 저장해야 할까?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;종종 ADR 소개 글에서는 Github 혹은 GitLab과 같은 원격 코드 저장소에서 관리하는 것을 추천한다. (프로젝트 소스 등과 함께)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이는 코드의 변화와 밀접한 연결성을 만들기 쉽고 버전 관리, PR리뷰 등 다양한 장점이 있지만 서비스 아키텍처에 걸친 의사결정이나 API 정의와 같은 공개 인터페이스 정의 의사결정의 경우 기록 장소가 모호해질 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 나는 팀에서 사용하는 위키/문서 도구를 이용하는 것이 문서 관리 일원화 측면에서 좋을 것 같다는 생각이 든다. 비록 코드와의 동기화가 어긋나기 쉽다지만, 이 부분은 노력의 영역이라 보며 결국 문서의 동기화 및 일원화는 무조건 필요한 부분이라고 본다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우리 팀에게 맞는 ADR은 뭘까?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR은 결국 근본적인 작성 원리는 있지만 정해져 있는 틀은 없다. 팀마다 문서 작성 시간이 여유 있는지 혹은 문서 관리 문화가 정착되어 있는지 혹은 업무 방식은 어떠한지 등 다르기 때문에, 팀에 맞는 ADR 작성법에 대해 몇 가지 정하고 정의하는 것이 좋다고 생각된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 템플릿은 어떤 항목을 포함할지(Context, Decision, Alternatives, Consequences 중 어디까지 쓸 것인가)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 문량은 어느 정도 작성할 것인지(한 페이지 이내? 제한 없이?)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 어떤 규모의 결정을 기록할지(되돌리기 어려운 결정만? 팀 전체에 영향을 주는 것만?)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 저장 위치는 어디로 할지(코드 저장소? 위키?)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;- 리뷰 프로세스는 어떻게 가져갈지(PR 리뷰에 포함? 주에 1번 리뷰? 승인은 누가?)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;등을 논의해서 결정하면 된다. 처음부터 완벽하게 정하려 하지 말고, 간단한 형식으로 시작해서 팀에 맞게 조금씩 다듬어 나가는 게 현실적인 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt; 우리 팀의 경우 비교적 빠른 개발 주기를 가지고 있다 그러하여 모든 문서를 정성 있게 쓰기에는 현실적으로 쉽지 않다. &lt;/span&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;문서는 컨플루언스를 메인으로 사용하고 코드와의 동기화 문화가 잘 형성되어 있다. 규모도 크지 않다. 이러한 상황들을 고려해서 우리는 간단하게 ADR을 어떻게 쓸 것인지 다음과 같이 정하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;- 코드 작성의 결정보다는 아키텍처의 설계 결정, 외부 공개 API 설계에 대한 결정만 작성하자!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;- 소통을 통한 결정은 모두 남지가!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;- 구성 요소는 배경,&amp;nbsp;&amp;nbsp;결정 사항, 결과로 하며, 문제의 원인과 결정에 대한 &quot;왜?&quot;를 명확히 작성하자!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;- 작성과 리뷰는 실무자 중심으로 유연하게 진행하자!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;- 모든 ADR 문서는 컨플루언스 팀 스페이스의 통합 ADR 폴더 하위에 모으자!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;- 최초 작성 이후 절대 수정은 없다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left; font-family: 'Noto Serif KR';&quot;&gt;등 &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ADR의 핵심 가치는 화려하고 있어 보이는 문서가 아니다. 미래에 우리에게 보내는 메시지의 일부라고 생각된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6개월 후의 나, 새로 합류한 동료, 이 프로젝트를 인수인계받을 누군가가 &quot;왜 이렇게 되어 있지?&quot;라고 물었을 때, 답을 줄 수 있어야 한다. 그 답이 &quot;당시에는 이런 상황이었고, 이런 이유로 이 선택을 했다&quot;일 수 있도록. ADR을 작성하면서 부담을 느낄 필요는 없을 것 같다. 중요한 건 꾸준한 습관을 만드는 것이다. 간단한 템플릿으로 시작해서, 팀의 필요에 맞게 조금씩 다듬어 나가면 된다. 오늘 내린 결정의 맥락을, 내일의 누군가를 위해 남겨두자.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765377515804&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;Why write ADRs&quot; data-og-description=&quot;How architecture decision records can help your team.&quot; data-og-host=&quot;github.blog&quot; data-og-source-url=&quot;https://github.blog/engineering/why-write-adrs/&quot; data-og-url=&quot;https://github.blog/engineering/architecture-optimization/why-write-adrs/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Suv0K/hyZO7LUGcw/YtGpuAptguRn1tOlyhfCak/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7H7pX/hyZPi00wiz/qND099qw198SKSorhfMKk0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cLpD7R/hyZOM2adqC/VGA5N0SwI3wzzDBMDjnkj1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://github.blog/engineering/why-write-adrs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.blog/engineering/why-write-adrs/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Suv0K/hyZO7LUGcw/YtGpuAptguRn1tOlyhfCak/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7H7pX/hyZPi00wiz/qND099qw198SKSorhfMKk0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cLpD7R/hyZOM2adqC/VGA5N0SwI3wzzDBMDjnkj1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Why write ADRs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How architecture decision records can help your team.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.blog&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;figure id=&quot;og_1765376364166&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;ADR 프로세스 - AWS 권장 가이드&quot; data-og-description=&quot;이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.&quot; data-og-host=&quot;docs.aws.amazon.com&quot; data-og-source-url=&quot;https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/architectural-decision-records/adr-process.html&quot; data-og-url=&quot;https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/architectural-decision-records/adr-process.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/U5RMd/hyZOKcd9jZ/i67Vz3T1bVBWqtJrPBJCGk/img.png?width=626&amp;amp;height=751&amp;amp;face=0_0_626_751,https://scrap.kakaocdn.net/dn/beSoxN/hyZPlXJZT5/LsoWBVrzg4ve7NLLFmaI6k/img.png?width=640&amp;amp;height=680&amp;amp;face=0_0_640_680,https://scrap.kakaocdn.net/dn/pZdJV/hyZOHmeYd9/p3g4EGbO4oGTDf1M5gBMM1/img.png?width=467&amp;amp;height=514&amp;amp;face=0_0_467_514&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/architectural-decision-records/adr-process.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/architectural-decision-records/adr-process.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/U5RMd/hyZOKcd9jZ/i67Vz3T1bVBWqtJrPBJCGk/img.png?width=626&amp;amp;height=751&amp;amp;face=0_0_626_751,https://scrap.kakaocdn.net/dn/beSoxN/hyZPlXJZT5/LsoWBVrzg4ve7NLLFmaI6k/img.png?width=640&amp;amp;height=680&amp;amp;face=0_0_640_680,https://scrap.kakaocdn.net/dn/pZdJV/hyZOHmeYd9/p3g4EGbO4oGTDf1M5gBMM1/img.png?width=467&amp;amp;height=514&amp;amp;face=0_0_467_514');&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;ADR 프로세스 - AWS 권장 가이드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.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;</description>
      <category>Backend/개발 방법론 &amp;amp; 디자인 패턴</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/103</guid>
      <comments>https://jangbageum.tistory.com/103#entry103comment</comments>
      <pubDate>Wed, 10 Dec 2025 23:41:52 +0900</pubDate>
    </item>
    <item>
      <title>[서평] Do it! 커서로 시작하는 AI 코딩 입문</title>
      <link>https://jangbageum.tistory.com/102</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;323&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ushqa/dJMb82r7V7c/eKUmJgnKrK4ultv0egZXP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ushqa/dJMb82r7V7c/eKUmJgnKrK4ultv0egZXP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ushqa/dJMb82r7V7c/eKUmJgnKrK4ultv0egZXP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fushqa%2FdJMb82r7V7c%2FeKUmJgnKrK4ultv0egZXP1%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;245&quot; height=&quot;318&quot; data-origin-width=&quot;323&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;[서평] Do it! 커서로 시작하는 AI 코딩 입문 | 고경희 - 이지스퍼블리싱&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;312&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;AI 코딩&amp;rdquo;, &amp;ldquo;바이브 코딩&amp;rdquo; &amp;hellip;&lt;/b&gt;&lt;br /&gt;요즘은 컴퓨터에 대한 깊은 이해가 없어도 생성형 AI를 이용해 동작하는 애플리케이션을 쉽게 만들 수 있다.&lt;br /&gt;이제는 아이디어만 있으면 단일 페이지 웹 서비스도 5분 만에 만들 수 있는 시대다.&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;312&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;569&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;나는 백엔드 개발을 업으로 하고 있다.&lt;br /&gt;2023년 초부터 폭발적으로 발전해온 AI를 보면서 놀라움과 동시에 큰 관심을 가졌지만, 한편으로는 &amp;ldquo;과연 저런 기술이 개발자까지 대체할 수 있을까?&amp;rdquo;라는 의문도 늘 있었다.&lt;/p&gt;
&lt;p data-end=&quot;569&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;682&quot; data-start=&quot;571&quot; data-ke-size=&quot;size16&quot;&gt;하지만 시간이 지나며 AI가 개발 영역까지 빠르게 확장되는 모습을 보면서,&lt;br /&gt;고지식했던 나는 &amp;lsquo;바이브 코딩&amp;rsquo;에 뒤처지고 있다는 위기감을 느꼈다.&lt;br /&gt;결국, 직접 경험해볼 필요가 있다고 판단했다.&lt;/p&gt;
&lt;p data-end=&quot;682&quot; data-start=&quot;571&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;765&quot; data-start=&quot;684&quot; data-ke-size=&quot;size16&quot;&gt;최근에 운 좋게도 《Do it! 커서로 시작하는 AI 코딩 입문》이라는 책을 읽을 기회가 생겼다.&lt;br /&gt;도서 표지 맨 위에는 이렇게 쓰여 있다.&lt;/p&gt;
&lt;blockquote data-end=&quot;795&quot; data-start=&quot;766&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;795&quot; data-start=&quot;768&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;세상의 속도를 따라잡고 싶다면 Do it!&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;867&quot; data-start=&quot;797&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;867&quot; data-start=&quot;797&quot; data-ke-size=&quot;size16&quot;&gt;나 역시 &amp;lsquo;바이브 코딩&amp;rsquo; 시대에 뒤처지지 않기 위해 이 책을 통해 &lt;b&gt;커서(Cursor)&lt;/b&gt; 를 직접 사용해 보기로 했다.&lt;/p&gt;
&lt;p data-end=&quot;867&quot; data-start=&quot;797&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;996&quot; data-start=&quot;869&quot; data-ke-size=&quot;size16&quot;&gt;또한 인프런에서 진행한&lt;br /&gt;&lt;b&gt;「Do it! 커서로 시작하는 AI 코딩 입문 5일 완독 챌린지」&lt;/b&gt; 도 함께 참여했다.&lt;br /&gt;책을 읽고 북토크에도 참여할 수 있으며, 완독 시 도서 1권을 제공해 주어 학습 동기부여에도 좋았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1760842960915&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;《Do it! 커서로 시작하는 AI 코딩 입문》 5일 완독 챌린지 챌린지 | kyunghee ko - 인프런&quot; data-og-description=&quot;코딩 초보자도 괜찮아요! AI와 함께라면 5일 만에 나만의 웹사이트를 만들 수 있어요. 저자 북토크에서 궁금한 점도 해결하세요!&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/challenge/do-it-%EC%BB%A4%EC%84%9C%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-ai-%EC%BD%94%EB%94%A9&quot; data-og-url=&quot;https://www.inflearn.com/challenge/do-it-%EC%BB%A4%EC%84%9C%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-ai-%EC%BD%94%EB%94%A9&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/FQUw1/hyZLlQicD6/1Fklokgh5KZjx8lKsen0r0/img.jpg?width=363&amp;amp;height=236&amp;amp;face=0_0_363_236,https://scrap.kakaocdn.net/dn/eehov9/hyZLm9wdUn/1KQZfmecQhYilvxF3qQni0/img.jpg?width=363&amp;amp;height=236&amp;amp;face=0_0_363_236,https://scrap.kakaocdn.net/dn/ePhQY/hyZLnHkLfi/Z54hlwBKSDqyx4XanqMdy0/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/challenge/do-it-%EC%BB%A4%EC%84%9C%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-ai-%EC%BD%94%EB%94%A9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/challenge/do-it-%EC%BB%A4%EC%84%9C%EB%A1%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-ai-%EC%BD%94%EB%94%A9&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/FQUw1/hyZLlQicD6/1Fklokgh5KZjx8lKsen0r0/img.jpg?width=363&amp;amp;height=236&amp;amp;face=0_0_363_236,https://scrap.kakaocdn.net/dn/eehov9/hyZLm9wdUn/1KQZfmecQhYilvxF3qQni0/img.jpg?width=363&amp;amp;height=236&amp;amp;face=0_0_363_236,https://scrap.kakaocdn.net/dn/ePhQY/hyZLnHkLfi/Z54hlwBKSDqyx4XanqMdy0/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_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;《Do it! 커서로 시작하는 AI 코딩 입문》 5일 완독 챌린지 챌린지 | kyunghee ko - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코딩 초보자도 괜찮아요! AI와 함께라면 5일 만에 나만의 웹사이트를 만들 수 있어요. 저자 북토크에서 궁금한 점도 해결하세요!&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.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;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1033&quot; data-start=&quot;1015&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;책의 커리큘럼은 다음과 같다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;1120&quot; data-start=&quot;1034&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1120&quot; data-start=&quot;1036&quot; data-ke-size=&quot;size16&quot;&gt;AI 코딩 이해하기 &amp;rarr; 커서로 첫 번째 웹사이트 만들기 &amp;rarr; 웹 개발의 전체 흐름 이해하기 &amp;rarr; 커서로 풀스택 웹앱 만들기 &amp;rarr; (부록) MCP 활용하기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1221&quot; data-start=&quot;1122&quot; data-ke-size=&quot;size16&quot;&gt;구성이 명확하고, AI 코딩을 통해 웹 개발의 기초를 다질 수 있도록 설계되어 있다.&lt;br /&gt;커서의 기본 사용법부터 웹 개발의 전체적인 흐름을 군더더기 없이 설명한 점이 좋았다.&lt;/p&gt;
&lt;p data-end=&quot;1221&quot; data-start=&quot;1122&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1301&quot; data-start=&quot;1223&quot; data-ke-size=&quot;size16&quot;&gt;분량이 많지 않아 지루하지 않으며,&lt;br /&gt;책 안에 포함된 &lt;b&gt;5일 완독 학습 계획표&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;KakaoTalk_20251019_120408476.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmsT8I/dJMb81Uh6QM/iWgS91K2a8QV8ngAZXaMN0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmsT8I/dJMb81Uh6QM/iWgS91K2a8QV8ngAZXaMN0/img.jpg&quot; data-alt=&quot;학습 계획표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmsT8I/dJMb81Uh6QM/iWgS91K2a8QV8ngAZXaMN0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmsT8I%2FdJMb81Uh6QM%2FiWgS91K2a8QV8ngAZXaMN0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;424&quot; height=&quot;565&quot; data-filename=&quot;KakaoTalk_20251019_120408476.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&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;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;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20251019_121520195.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwol7Z/dJMb9N9Dm8v/ScmIMkrZokxJDql43q2hDk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwol7Z/dJMb9N9Dm8v/ScmIMkrZokxJDql43q2hDk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwol7Z/dJMb9N9Dm8v/ScmIMkrZokxJDql43q2hDk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwol7Z%2FdJMb9N9Dm8v%2FScmIMkrZokxJDql43q2hDk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;529&quot; height=&quot;705&quot; data-filename=&quot;KakaoTalk_20251019_121520195.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20251019_121520195_01.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebbgD3/dJMb9OHszFF/CN2rSKAv24XrYgqJ64kCw0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebbgD3/dJMb9OHszFF/CN2rSKAv24XrYgqJ64kCw0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebbgD3/dJMb9OHszFF/CN2rSKAv24XrYgqJ64kCw0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebbgD3%2FdJMb9OHszFF%2FCN2rSKAv24XrYgqJ64kCw0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;4000&quot; data-filename=&quot;KakaoTalk_20251019_121520195_01.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;첫째 마당. AI 코딩 시작하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에 대한 대략적인 내용을 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI발전에 대한 내용, AI 서비스, LLM, 프롬프트 작성 요령, AI코딩으로 할 수 있는 것들 등&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;커서를 이용해서 웹 사이트를 만들기 위해서는 해당 내용만 이해하고 있으면 AI를 사용하는 데 있어 충분하다. 만약 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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;둘째 마당. 커서로 웹사이트 만들기&lt;/b&gt;&lt;/span&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커서에 최초 가입하게 되면 Pro Trial 플랜을 받게 되는데, 7일간 무료로 사용할 수 있다.&lt;/b&gt; 7일 안에는 책을 완독 할 수 있다 생각하기에 무료 플랜을 이용해서 실습하기에는 충분하다 생각한다.&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;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9p5It/dJMb80A4Uvv/DMZvZhF7uivYjLVKwuUbd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9p5It/dJMb80A4Uvv/DMZvZhF7uivYjLVKwuUbd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9p5It/dJMb80A4Uvv/DMZvZhF7uivYjLVKwuUbd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9p5It%2FdJMb80A4Uvv%2FDMZvZhF7uivYjLVKwuUbd1%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;152&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서는 우리가 흔히 사용하는 VSCode를 포크 하여 만들어졌다. 그러하여 사용법은 매우 친근했다. 또 기존 개인적으로 사용하던 VSCode의 확장 프로그램들도 자동으로 import 가 가능하다.&lt;/p&gt;
&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;&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;실전에 들어가기 전, 흔히 HCJ 스택이라 부르는 HTML, CSS, Javascript에 대한 기초를 설명하고 Git과 Github에 대한 사용법도 일부 설명한다.&lt;/p&gt;
&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;마지막으로 Github Page를 통해서 내가 만든 웹 사이트를 Public 하게 배포하는 과정도 있다. 웹 개발이 처음인 분들에게는 재밌는 경험이 될 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;셋째 마당. 커서로 풀스택 앱 만들기&lt;/b&gt;&lt;/span&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;/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;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;부록. MCP 서버 활용하기&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP에 대한 간략한 설명과 실습을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&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 분야가 얼마나 빠르게 변하고 있는가 체감 할 수 있는 시간&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;나 같은 고집 있는 개발자들은 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;/p&gt;
&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;br /&gt;&lt;b&gt;커서를 직접 체험하고 싶은 개발자&lt;/b&gt;에게도 충분히 가치 있다.&lt;br /&gt;나 역시 커서의 강력함을 경험하며 흥미롭게 완독했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책은 다음과 같은 사람들에게 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 웹 개발을 입문하는 사람&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;
&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;&amp;ldquo;AI를 이용해 웹 개발이 가능하다&amp;rdquo;&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;또 AI를 이용하여 만든 웹 앱은 공개적으로 무지성 배포를 진행하면 안 된다. 서비스를 운영하기 위해서는&lt;b&gt; 기본적인 보안 지식과 운영 비용에 대한 지출 효율화&lt;/b&gt;가 필요하다. 이러한 부분들도 함께 챙길 수 있는 학습을 진행하면 좋을 것 같다.&lt;/p&gt;</description>
      <category>서평</category>
      <category>AI 코딩</category>
      <category>Cursor</category>
      <category>Do It!</category>
      <category>IDE</category>
      <category>바이브코딩</category>
      <category>웹 개발</category>
      <category>웹 앱</category>
      <category>이지스퍼블리싱</category>
      <category>커서</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/102</guid>
      <comments>https://jangbageum.tistory.com/102#entry102comment</comments>
      <pubDate>Sun, 19 Oct 2025 14:12:47 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 일관된 해싱(Consistent Hasing), 그리고 분산 환경에서 메시지 라우팅을 위한 구조 고민</title>
      <link>https://jangbageum.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리는 &lt;b&gt;클러스터링 된 WebSocket 서버 환경&lt;/b&gt;에서 &lt;b&gt;실시간 메시징 기능&lt;/b&gt;을 제공하고 있다. 클라이언트 수가 증가함에 따라 &lt;b&gt;WebSocket 서버의 수평 확장&lt;/b&gt;이 자연스럽게 필요해졌고, 이에 따라 다음과 같은 메시지 라우팅 문제를 고민하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어,&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사용자 A는 서버 1에 접속하고,&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사용자 B는 서버 3에 접속해 있다고 하자.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이때 A가 B에게 메시지를 보내려면 &lt;b&gt;B가 연결된 서버를 알아야 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;즉, 클러스터 환경에서는 &lt;b&gt;&quot;어느 서버에 누구(userId)가 붙어 있는지&quot;&lt;/b&gt;에 대한 &lt;b&gt;라우팅 정보 관리&lt;/b&gt;가 핵심이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 문제를 해결하기 위한 대표적인 방법은 다음과 같다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Redis 등에 라우팅 테이블을 유지&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Gateway 서버에서 중앙 집중형 라우팅 처리&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Consistent Hashing 기반 라우팅&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리는 가능한 한 &lt;b&gt;모든 영역에서 상태를 갖지 않으면서도 메시지가 정확히 라우팅 되기를&lt;/b&gt; 원했기에, Consistent Hashing 개념에 주목하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;1. 기존 해싱 방식의 구조적 한계&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전통적으로 분산 시스템에서는 아래와 같은 방식으로 노드를 분배한다:&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;hash(userId) % N   // N은 서버 수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;hash(&quot;user1234&quot;) % 3 = 1 &amp;rarr; 1번 서버&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;하지만 서버 수가 변경되면 대부분의 키 매핑이 바뀌게 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예: hash(&quot;user1234&quot;) % 4 = 3 &amp;rarr; 3번 서버&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 방식의 문제점은 다음과 같다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버가 추가/제거되면 대부분의 클라이언트가 다른 서버로 매핑됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;캐시라면 캐시 미스 발생 &amp;rarr; 성능 저하&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;WebSocket이라면 사용자 연결 예측 실패 &amp;rarr; 메시지 유실 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2. Consistent Hashing의 기본 개념&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Consistent Hashing은 위 문제를 해결하기 위한 구조적 해법이다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전체 해시 공간을 &lt;b&gt;원형(링 구조)으로&lt;/b&gt; 가정한다 (예: 0 ~ 2&amp;sup3;&amp;sup2;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;서버와 키(예: userId)&lt;/b&gt; 모두 해시하여 이 원 위에 위치시킨다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;각 키는 &lt;b&gt;시계 방향으로 가장 가까운 서버&lt;/b&gt;에 할당된다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;요소 해시 각도 역할&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6UUXA/btsOQyuvZfr/kqrNgqt4vNF9ikZJFFZVu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6UUXA/btsOQyuvZfr/kqrNgqt4vNF9ikZJFFZVu1/img.png&quot; data-alt=&quot;https://en.wikipedia.org/wiki/Consistent_hashing&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6UUXA/btsOQyuvZfr/kqrNgqt4vNF9ikZJFFZVu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6UUXA%2FbtsOQyuvZfr%2FkqrNgqt4vNF9ikZJFFZVu1%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;611&quot; height=&quot;455&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;455&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://en.wikipedia.org/wiki/Consistent_hashing&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 66.2791%; height: 69px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.2806%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: center;&quot;&gt;Server A&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.9649%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: center;&quot;&gt;74&amp;deg;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.5838%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: center;&quot;&gt;서버 노드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.2806%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;Server B&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.9649%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;139&amp;deg;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.5838%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: center;&quot;&gt;서버 노드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.2806%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;Server C&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.9649%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;310&amp;deg;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.5838%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: center;&quot;&gt;서버 노드&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.2806%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;BLOB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.9649%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;111&amp;deg;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 29.5838%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;Server B로 매핑됨&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2.1. 서버 추가 시 변화&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;서버 D가 200&amp;deg;에 추가된다면?&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존 B~C 사이 키 중 일부만 D로 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;나머지 매핑은 &lt;b&gt;그대로 유지&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;즉, 서버가 추가되더라도 &lt;b&gt;데이터 이동량이 최소화&lt;/b&gt;됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 특징은 &lt;b&gt;확장성과 안정성&lt;/b&gt; 측면에서 매우 유리하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;3. 실무 적용에 대한 현실적 고민&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Consistent Hashing을 기반으로 메시지를 라우팅 하면,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;동일한 사용자에게 가는 메시지는 항상 동일한 WebSocket 서버로 전송될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 &lt;b&gt;한 가지 중요한 전제가 필요하다:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&quot;사용자가 항상 자신에게 할당된 서버에 접속해 있어야 한다.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;즉, 클라이언트 접속 시에도 hash(userId)를 기준으로 &lt;b&gt;&quot;정해진 서버&quot;로 유도&lt;/b&gt;되어야 한다. 이를 흔히 &lt;b&gt;Sticky Routing&lt;/b&gt; 또는 &lt;b&gt;Pre-Routing&lt;/b&gt;이라고 하며, L4/L7 로드 밸런서에서 일부 지원하긴 하지만 제약이 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;현실적으로는 사용자 접속이 랜덤 하게 이루어지는 경우가 많기 때문에,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Consistent Hashing만으로는 사용자 위치를 정확히 예측하기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;4. 우리의 선택은?&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 현실적인 제약을 고려해, 우리는 다음과 같은 구조로 설계를 진행하고 있다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;클라이언트 접속 시 WebSocket 서버는 사용자 ID를 Redis에 등록&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;A &amp;rarr; B 메시지를 보낼 때 Redis에서 B의 현재 서버를 조회&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;메시지를 해당 서버로 Pub/Sub 또는 직접 전달&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 방식은 상태를 최소화하면서도 &lt;b&gt;정확한 라우팅&lt;/b&gt;을 가능하게 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;추가적으로 Redis 자체를 클러스터링 하거나 TTL을 두어 장애 대응도 병행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Consistent Hashing은 &lt;b&gt;노드 변경 시에도 대부분의 매핑을 유지할 수 있는 매우 효율적인 알고리즘&lt;/b&gt;이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러나 실무에서는 &lt;b&gt;클라이언트 접속 방식&lt;/b&gt;, &lt;b&gt;네트워크 구조&lt;/b&gt;, &lt;b&gt;로드 밸런서의 특성&lt;/b&gt; 등 다양한 현실적인 제약을 함께 고려해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;사용자가 항상 정해진 서버로 접속할 수 있다면&lt;/b&gt;, Consistent Hashing은 강력한 해법이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;그렇지 않다면&lt;/b&gt;, Redis 기반의 동적 라우팅 + Pub/Sub 구조가 더 유연하고 실용적이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;아키텍처 설계에서 중요한 것은 &lt;b&gt;특정 기술을 적용하는 것 자체&lt;/b&gt;가 아니라,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;현재 시스템의 특성과 제약 조건에 맞는 적절한 방법을 선택하는 것&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Consistent Hashing은 그 자체로 멋있는 개념이지만,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;우리의 서비스 환경에서는 &lt;b&gt;Redis 기반 라우팅이 더 현실적이고 안정적인 해법&lt;/b&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;궁극적으로 기술은 &quot;이론&quot;보다 &quot;현실&quot;에서의 타협을 하는 것이 중요한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&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;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Consistent_hashing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Consistent_hashing&lt;/a&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%80%EB%90%9C_%ED%95%B4%EC%8B%B1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%80%EB%90%9C_%ED%95%B4%EC%8B%B1&lt;/a&gt;&lt;/p&gt;</description>
      <category>Backend/개발 방법론 &amp;amp; 디자인 패턴</category>
      <category>Consistent Hashing</category>
      <category>알고리즘</category>
      <category>일관된 해싱</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/101</guid>
      <comments>https://jangbageum.tistory.com/101#entry101comment</comments>
      <pubDate>Wed, 25 Jun 2025 22:13:23 +0900</pubDate>
    </item>
    <item>
      <title>[Go] goroutine은 어떻게 작동하는가: 왜 빠르다는 건데?</title>
      <link>https://jangbageum.tistory.com/100</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Go 언어를 사용하지 않아도 goroutine이라는 단어는 한 번쯤 들어봤을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;가볍다&quot;, &quot;수십만 개도 문제없다&quot;, &quot;스레드보다 효율적이다&quot; 같은 말이 항상 따라 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제로 goroutine이 어떻게 동작하길래 이련 평가를 받는가 라는 궁금증이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Go 언어를 사용하는 개발자는 아니다. 하지만 주로 사용하지 않는 언어 혹은 기타 기술들의 동작 원리와 녹아있는 방법론, 사상을 공부하는 것은 늘 영갑을 주고 실제 개발을 할 때 적용할 수 있는지 고민하는데 정말 큰 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 goroutine처럼 기존 병렬성 개념을 재해석한 구조를 깊이 들여다보면, 언어를 넘어서 시스템 설계나 성능 최적화에 대한 감각을 키우는 데에도 큰 도움이 될 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 goroutine이 어떻게 작동하고, 왜 그렇게 설계되었고, 실제 어떤 방식으로 메모리와 스레드를 사용하는지를 기록하며 설명하려고 한다.&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;676&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3QoSp/btsOMaFpiBf/ABWEwkrzj9L16M9ePzVD41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3QoSp/btsOMaFpiBf/ABWEwkrzj9L16M9ePzVD41/img.png&quot; data-alt=&quot;https://syslog.me/2018/05/23/concurrency-in-go/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3QoSp/btsOMaFpiBf/ABWEwkrzj9L16M9ePzVD41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3QoSp%2FbtsOMaFpiBf%2FABWEwkrzj9L16M9ePzVD41%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;676&quot; height=&quot;483&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://syslog.me/2018/05/23/concurrency-in-go/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;1. goroutine 이란?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;goroutine&lt;/b&gt;은 Go 언어에서 함수를 &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;- 재밌게도 `go` 키워드만 붙이면, 어떤 함수든 goroutine으로 실행된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;2. 어떻게 가볍게 작동할 수 있는가?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 런타임의 고유한 &lt;b&gt;M:N 스케줄링 구조&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;여기서 말하는 M:N 스케줄링은 &lt;b&gt;&quot;M가의 사용자 수준의 실행 단위(goroutine)를 N개의 커널 수준의 실행 단위(OS 스레드) 위에 Go 런타임이 직접 스케줄링해 매핑하는 방식&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;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 22.907%; height: 20px;&quot;&gt;&lt;b&gt;구성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77.093%; height: 20px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 22.907%; height: 20px;&quot;&gt;&lt;b&gt;G&lt;/b&gt; (goroutine)&lt;/td&gt;
&lt;td style=&quot;width: 77.093%; height: 20px;&quot;&gt;우리가 만든 실행 단위. 함수를 실행하는 컨텍스트 (t사용자 수준의 실행 단위)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.907%; height: 17px;&quot;&gt;&lt;b&gt;P&lt;/b&gt; (Processor)&lt;/td&gt;
&lt;td style=&quot;width: 77.093%; height: 17px;&quot;&gt;실행 가능한 goroutine 자원을 관리하는 논리적 유닛&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.907%; height: 17px;&quot;&gt;&lt;b&gt;M&lt;/b&gt; (Machine)&lt;/td&gt;
&lt;td style=&quot;width: 77.093%; height: 17px;&quot;&gt;실제 OS 스레드, G를 CPU에 올리는 일꾼&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;&quot;P&quot;는 goloutine의 실행 큐를 관리하며 스케줄링 및 자원 할당을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;P&quot;는 실행을 기다리고 있는 goroutine들의 로컬 큐를 관리한다. 이 큐에는 아직 M에 의해 실행되지 않은 goroutine들이 들어있다. &quot;P&quot;는 자신의 로컬 큐에 있는 goroutine을 M에 할당하여 실행한다. &quot;P&quot;는 또한 다른 &quot;P&quot;의 큐에서 G를 훔쳐오기를 통해 균형을 맞춘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런타임은 G -&amp;gt; P -&amp;gt; M 구조로 goroutine을 관리하며, OS 스레드 수보다 훨씬 많은 goroutine을 동시에 운용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 구조는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sB7Ck/btsOJKVMTFc/9tgzViOfs72KklVGkx9Z5k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sB7Ck/btsOJKVMTFc/9tgzViOfs72KklVGkx9Z5k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sB7Ck/btsOJKVMTFc/9tgzViOfs72KklVGkx9Z5k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsB7Ck%2FbtsOJKVMTFc%2F9tgzViOfs72KklVGkx9Z5k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;875&quot; height=&quot;421&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;&lt;b&gt;구성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;&lt;b&gt;LRQ&lt;/b&gt; (Local Run Queue)&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;각 P가 자체 관리하는 작업 대기열이다. P와 연결된 M은 해당 LRQ에 있는 goroutine을 가져와 실행한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;&lt;b&gt;GRQ&lt;/b&gt; (Global Run Queue)&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;모든 P가 공유하는 큐이다. P는 자신의 LRQ가 비게 되면 GRQ에 있는 goroutine을 자신의 LRQ에 넣는다.&lt;br /&gt;GRQ에는 LRQ에 속하지 않는 G들이 존재하고 G가 LRQ에 소속되지 않는 이유는 다양하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;2.2. goroutine의 병렬성은 어떻게 관리될까?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구성들로 확인했던 것과 같이. Go의 병렬성은 &lt;b&gt;OS 스레드가 아닌 Go 런타임이 직접 관리하는 스케줄러&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;- 각 P는 자신의 goroutine 큐를 관리한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- M이 P로부터 goroutine을 받아 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- P가 일이 없으면 다른 P로부터 goroutine을 훔쳐오는 구조 가짐&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- I/O 등으로 goroutine이 블로킹되면, 다른 M이 남은 작업을 처리&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;구조는 대충 알았으나 이러한 구조가 왜 goloutine을 가볍게 운용할 수 있도록 했을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 아래에서 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;2.2.1 OS 스레드를 직접 만들지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 언어에서는 병렬 처리를 위해 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;b&gt;- 하나 만들면 보통 1MB 이상의 고정 스택 메모리가 필요&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;반면 Go는:&lt;/p&gt;
&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;- goroutine이 OS 스레드에 직접 묶이지 않고, Go 런타임이 자체 스케줄러를 통해 다수의 goroutine을 적은 수의 OS 스레드로 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;2.2.2. 스택을 작게 시작하고, 필요할 때만 키운다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goroutine은 &lt;b&gt;2KB의 스택 메모리로 시작&lt;/b&gt;한다. 만약 부족하면 스택을 자동으로 확장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;2.2.3. 유저레벨 스케줄링으로 context switching이 빠르다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goroutine 사이의 전환은 OS가 아니라 Go 런타임이 직접 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- OS 스레드처럼 커널에 개입을 요청하지 않고, Go 런타임이 G를 P에 넣고 M이 꺼내서 실행&lt;/p&gt;
&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;- 락 없는 로컬 큐 처리 (LRQ)로 빠른 작업 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 글로벌 큐 (GRQ)로 작업 불균형 해소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- Work Stealing으로 유효 P가 다른 P의 goroutine을 실행 가능&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;결과적으로 수천수만 개의 goroutine이 적은 스레에서 유기적으로 잘 돌아가는 병렬 처리 환경이 만들어진다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;3. goroutine의 스택은 왜 2KB밖에 안 될까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급되었지만 기존 OS 스레드는 1MB 이상의 스택을 미리 예약한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 비해 goroutine은 시작 시 단 2KB의 스택을 사용한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 go가 스택을 &lt;b&gt;필요할 때만 동적으로 키우는 구조&lt;/b&gt;로 설계되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;3.1. 동작 방식 (Stack Split)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. goroutine은 2KB의 작은 스택으로 시작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 함수 호출이 깊어지거나 지역 변수가 많아져 스택이 부족하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Go 런타임은 새로운 더 큰 스택을 힙에 할당하고, 기존 스택의 내용을 복사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 함수 실행을 새 스택에서 계속 이어간다.&lt;/p&gt;
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;4. 스택 복사는 느리지 않을까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go의 stack split은 현실적으로 문제가 되지 않는다고 한다. 이유는 아래와 같다.&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;- 대부분의 goroutine은 짧고 단순한 함수 실행만 포함하므로, 2KB 스택으로 충분하다.&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;goroutine 스택은 연속된 메모리 블록이기 때문에, 복사 연산이 매우 빠르고 CPU 캐시 친화적이다.&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;shrink도 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 시점의 Go 버전 이후부터는 늘어난 스택 메모리를 GC 타이밍에 자동으로 줄어들기도 한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;5. goroutine은 언제 확장될까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 상황에서 stack split이 발생할 수 있다:&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;- 함수 call chain이 길어질 때&lt;/p&gt;
&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;6. 그래서 goroutine이 왜 좋은가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 정리하면 goroutine은 단순히 &quot;작은 스레드&quot;의 운용이 아닌 자체가 하나의 런타임 시스템이며 아래와 같은 특징이 있다고 할 수 있을 것 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;가벼운 메모리 사용&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;시작 시 2KB -&amp;gt; 필요 시만 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;자동 스케줄링&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;M:N 구조 + work stealing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;수십만 개 실행 가능&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;OS 스레드보다 메모리 부담 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;고성능&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;대부분의 goroutine은 확장 없이 실행되며, 복사도 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;개발자 경험&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;`go func()` 하나로 병렬 처리 가능, 간결함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 35.1163%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 64.8837%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;goroutine은 단지 성능 좋다로 끝나는 것이 아니라, Go 언어가 지향하는 단순함, 병렬성, 안정성 등의 철학을 잘 보여주는 대표 사례인 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그 내부에는 스케줄러, 메모리 관리, 어셈블리 최적화 등 복잡한 기술이 숨어있지만, 개발자는 이를 알지 못해도 아주 쉽게 병렬 프로그램을 만들 수 있다. 이러한 구조를 이해하고 나면, Go를 쓰지 않더라고 goroutine이 제시하는 방식은 다른 언어에서 병렬성을 설계하거나 효율적인 런타임 구조를 고민할 때 깊은 영감을 줄 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 주로 사용하는 Node.js는 이벤트 루프, 비동기 콜백 등의 개념을 포함하여 싱글 스레드로 동작하고 있다. Go와 비교했을 때, 병렬성 문제를 서로 전혀 다른 철학으로 풀고 있다는 점이 인상 깊었고 중요한 부분이 아닐까 싶다.&lt;/p&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;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(블로그) goroutine 설계 관련 - &lt;a href=&quot;https://go.dev/blog/concurrency-is-not-parallelism&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://go.dev/blog/concurrency-is-not-parallelism&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(블로그) goroutine 스케줄링 관련 - &lt;a href=&quot;https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(github) goroutine 구조체 - &lt;a href=&quot;https://github.com/golang/go/blob/master/src/runtime/proc.go&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/golang/go/blob/master/src/runtime/proc.go&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <category>go</category>
      <category>golang</category>
      <category>goroutine</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/100</guid>
      <comments>https://jangbageum.tistory.com/100#entry100comment</comments>
      <pubDate>Fri, 20 Jun 2025 16:51:18 +0900</pubDate>
    </item>
    <item>
      <title>[RabbitMQ] '메시지 발행자' 입장에서의 메시지 배달 보장과 성능의 절충점</title>
      <link>https://jangbageum.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;담당하여 운영 중인 도메인 서비스에서 RabbitMQ로 이벤트 메시지를 발행하는 과정을 하드하게 운영하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 메시지는 주문/결제 등 고객 서비스에 직결되는 데이터였기 때문에, 단 한 건의 메시지도 누락되면 안 된다는 목표를 가질 수밖에 없었다.&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;그래서 이번 과정에서 RabbitMQ의 메시지 발행자 입장에서 가능한 메시지 배달 보장 방식들을 전부 정리해 보고, 각 단계의 장단점과 성능 비용에 대한 감을 잡고 서비스에 맞는 골디락스 존을 찾고자 한다.&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;1280&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QBcUF/btsOA3uv0ID/GBdeKhJ2KWSNcS1ftehKn1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QBcUF/btsOA3uv0ID/GBdeKhJ2KWSNcS1ftehKn1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QBcUF/btsOA3uv0ID/GBdeKhJ2KWSNcS1ftehKn1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQBcUF%2FbtsOA3uv0ID%2FGBdeKhJ2KWSNcS1ftehKn1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;488&quot; height=&quot;358&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;# RabbitMQ 발행자 입장에서의 메시지 배달 보장 8단계&lt;/b&gt;&lt;/span&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;메시지 배당 보장 수준은 8단계로 나누어 정리해 보았다. 각 기능은 독립적인 기능이며 함께 조합함으로써 보장 수준을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;## 배당 보장 8단계&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계. 보장하지 않음 (Fire-and-Forget)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: 실패 통보받기 (mandatory 옵션 + return 리스너)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3단계: 발행자 확인 (Publisher Confirms)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4단계 대체 익스체인지 (Alternate Exchange)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5단계: HA큐 (Hight Availabilty Queue)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6단계: 트랜잭션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7단계: 트랜잭션 HA 큐&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8단계: 메시지 디스크에 저장 (Durable Queue + Delivery-Mode:2)&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;이어서 각 배달 보장 8단계에 대해 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드의 경우 내가 주로 사용하는 Node.js의 amqplib 모듈을 사용하여 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 1단계. 보장하지 않음 (Fire-and-Forget)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 방식이다. 그냥 `channel.publish()` 를 호출하면 끝이다.&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;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 2단계: 실패 통보받기 (mandatory 옵션 + return 리스너)&lt;/b&gt;&lt;/span&gt;&lt;/h4&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 발행 시, &lt;b&gt;`mandatory: true`&lt;/b&gt; 옵션을 설정하면, 라우팅 실패 시 &lt;b&gt;`return` 이벤트&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;라우팅 문제가 있을 수 있는 환경이라면, 최소한 이 정도 설정은 해두는 것이 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 익스체인지와 큐가 바인딩 돼있지 않다고 했을 때, return 이벤트를 핸들링하는 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1749903738367&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const amqp = require('amqplib');

(async () =&amp;gt; {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();

  const exchange = 'test_exchange';
  const routingKey = 'not_exist_queue'; // 이 키에 매칭되는 큐가 없다고 가정

  await channel.assertExchange(exchange, 'direct', { durable: false });

  // return 이벤트 리스너 등록 (반환된 메시지 처리)
  channel.on('return', (msg) =&amp;gt; {
    // 별도 처리 로직 구현 필요
  });

  const success = channel.publish(
    exchange,
    routingKey,
    Buffer.from('Hello, this may fail to be routed!'),
    {
      mandatory: true // 큐에 라우팅되지 않으면 return으로 알림 받음
    }
  );

  console.log(`Message published (mandatory: true): ${success}`);
})();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 3단계: 발행자 확인 (Publisher Confirms)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;발행자가 확실히 메시지를 전달했는지를 확인&lt;/b&gt;하려면, RabbitMQ의 Confirm 모드를 사용하는 것이 좋을 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메시지를 보낸 후, &lt;b&gt;브로커로부터 ack(성공) 또는 nack(실패) 신호를 받게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비동기로 처리할 수 있고, &lt;b&gt;`waitForConfirms()`&lt;/b&gt;를 통해 순차적으로 보장할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 ack가 오지 않거나 nack가 오면, 이를 감지하여 별도의 처리 로직을 동작시킬 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 confirmChannel을 생성하고 waitForConfirms()를 사용하는 예시이다.&lt;/p&gt;
&lt;pre id=&quot;code_1749978123494&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const amqp = require('amqplib');

(async () =&amp;gt; {
  const conn = await amqp.connect('amqp://localhost');

  // ConfirmChannel을 생성해야 함
  const channel = await conn.createConfirmChannel();

  const exchange = 'confirm_exchange';
  const routingKey = 'valid_key';

  await channel.assertExchange(exchange, 'direct', { durable: true });

  // 메시지 발행
  const success = channel.publish(
    exchange,
    routingKey,
    Buffer.from('This message should be confirmed by broker'),
    { persistent: true } // durable queue와 함께 쓰면 디스크에 저장됨
  );

  console.log(`Message sent to exchange: ${success}`);

  // 브로커의 ACK/NACK 확인
  try {
    await channel.waitForConfirms();
    console.log('Broker acknowledged message (ack)');
  } catch (err) {
    console.error('Broker did not confirm message (nack)');
    console.error(err);
  }

  await channel.close();
  await conn.close();
})();&lt;/code&gt;&lt;/pre&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;&lt;b&gt;`waitForConfirms()`&lt;/b&gt; 는 채널의 모든 publish에 대한 confirm을 기다리므로 메시지 발행량이 많다면 비효율적일 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고성능이 요구되는 시스템에서는 `channel.publish()` 시, `channel.once('ack', callback)` 을 활용한 비동기 처리&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 4단계 대체 익스체인지 (Alternate Exchange)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메시지가 어디에도 전달되지 못했을 때, 그냥 버릴 것이 아니라 &lt;b&gt;대체 익스체인지&lt;/b&gt;로 전달되도록 설정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`x-alternate-exchange`&lt;/b&gt; 를 설정해 두면, 라우팅 실패 메시지를 지정된 fallback exchange로 보내 후속 처리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실패 메시지를 놓치지 않고 관리하고 싶다면 좋은 선택으로 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1749978645448&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const amqp = require('amqplib');

(async () =&amp;gt; {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();

  const mainExchange = 'main_exchange';
  const altExchange = 'alternate_exchange';
  const validRoutingKey = 'valid_key';
  const invalidRoutingKey = 'invalid_key';
  const failedQueue = 'unroutable_messages';

  // 1. 대체 Exchange 먼저 선언
  await channel.assertExchange(altExchange, 'fanout', { durable: true });

  // 2. 대체 Exchange에 연결될 큐 선언
  await channel.assertQueue(failedQueue, { durable: true });
  await channel.bindQueue(failedQueue, altExchange, '');

  // 3. 주 Exchange 선언하면서 x-alternate-exchange 설정
  await channel.assertExchange(mainExchange, 'direct', {
    durable: true,
    arguments: {
      'x-alternate-exchange': altExchange
    }
  });

  // 4. 주 Exchange에 연결되는 정상 큐 (optional)
  // await channel.assertQueue('valid_queue');
  // await channel.bindQueue('valid_queue', mainExchange, validRoutingKey);

  // 5. 유효하지 않은 라우팅 키로 메시지 전송 (fallback 발생)
  const msg = 'This message will be routed to alternate exchange';
  channel.publish(mainExchange, invalidRoutingKey, Buffer.from(msg), {
    persistent: true
  });

  console.log(`Sent message with invalid routingKey &amp;rarr; should go to [${failedQueue}]`);

  // 확인을 위해 fallback 큐에서 메시지 수신 (1초 대기)
  setTimeout(async () =&amp;gt; {
    const msg = await channel.get(failedQueue, { noAck: true });
    if (msg) {
      console.log(`Received in fallback queue: ${msg.content.toString()}`);
    } else {
      console.log(`No message in fallback queue.`);
    }
    await channel.close();
    await conn.close();
  }, 1000);
})();&lt;/code&gt;&lt;/pre&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;대체 익스체인지로 가는 메시지에는 원래 라우팅 키와 익스체인지 정보가 유지된다. 이는 후처리 시, 활용이 가능하며 fallback exchange에도 바인딩된 큐가 없으면 이 메시지는 유실되어 버린다.&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;&lt;b&gt;#### 대체 익스체인지와 mandatory 옵션을 함께 사용할 때의 주의점&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대체 익스체인지와 실패 통보(return 이벤트)를 받을 수 있는 mandatory:true 옵션을 함께 사용할 시 중요하게 인지해야 하는 점이 있다.&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;&lt;b&gt;- mandatory:true와 대체 익스체인지가 설정된 경우, 메시지는 반환되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;mandatory 플래그를 true로 주고 메시지를 발행해도, 익스체인지에서 대체 익스체인지가 지정되어 있으면 라우팅 되지 못한 메시지는 발행자에게 반환(Basic.Return)되지 않고 대체 키로 전달된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 발행자가 라우팅 실패를 감지하고 싶어서 mandatory를 true로 설정했다면, 대체 익스체인지가 있으면 이 동작이 일어나지 않으니 주의해야 한다.&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;&lt;b&gt;- 대체 익스체인지도 라우팅 실패 시 메시지 손실이 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;이 경우에도 발행자에게 Basic.Return이 전달되지 않는다.&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;그렇기에 실무에서 사용할 시에는 대체 익스체인지와 mandatory:true를 동시에 사용할 때는 메시지 반환 (Basic.Return) 이벤트가 발생하지 않는다는 점을 반드시 인지해야 하고, 대체 익스체인지에 큐가 바인딩되어 있지 않으면 메시지가 손실될 수 있으니, 대체 익스체인지의 큐 바인딩 상채를 항상 확인할 수 있어야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 5단계: HA큐 (Hight Availabilty Queue)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 노드 간 클러스터링이 가능하지만, 하나의 노드에 장애가 나면 큐와 메시지도 함께 사라질 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 방지하려면 HA 큐의 사용을 고려해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RabbitMQ 클러스터는 여러 노드로 구성되어 있지만, &lt;b&gt;기본적으로 큐와 메시지는 생성된 노드에만 저장&lt;/b&gt;된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 한 노드에 장애가 나면 해당 노드의 큐와 메시지도 함께 사라질 수 있다. 그러하여 서비스 &lt;b&gt;안정성을 위해선 큐와 메시지를 여러 노드에 복제&lt;/b&gt;해 두는 HA 큐가 필수라고 할 수 있다.&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;HA 큐의 종류는 &lt;b&gt;Mirrored Queue&lt;/b&gt;와 &lt;b&gt;Quorum Queue&lt;/b&gt; 가 존재한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;큰 차이로는 이전 방식과 &lt;b&gt;최신 방식이라는 점과 알고리즘의 차이&lt;/b&gt;가 있다. 현재는 &lt;b&gt;Quorum Queue 방식을 권장&lt;/b&gt;하고 있고, 분산 합의 알고리즘인 &lt;b&gt;Raft 알고리즘&lt;/b&gt;을 사용한다는 점이다. 각 방식에 대해서는 자세히 다루지는 않겠다. 이후 설명은 Quorum Queue 기반으로 설명한다.&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;메시지는 큐에 도착하는 시점에서 연결된 노드(리더+팔로워)에 동시에 저장된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Raft 분산 합의 알고리즘을 사용한다는 점에서 &lt;b&gt;과반수 노드에 메시지가 저장되어야만 메시지 발행이 성공(ACK)으로 처리&lt;/b&gt;된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 구조 덕분에 일부 노드 장애가 발생해도, &lt;b&gt;과반수 노드가 살아 있으면 메시지 유실 없이 복구&lt;/b&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;실무적으로는 HA 큐를 사용할 때, 고려할 점은 아래와 같을 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 신규 시스템에는 반드시 Quorum Queue를 사용 (큐 생성 시, `x-queue-type: quorum` 옵션 추가)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 복제 노드 수는 기본 3개, 중요도에 따라 노드 수를 올리되 홀수로 세팅&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스가 클러스터 환경이라면 HA 큐는 필수로 고려해야 할 보장 수준일 것 같다.&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;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.rabbitmq.com/blog/2020/04/20/rabbitmq-gets-an-ha-upgrade&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.rabbitmq.com/blog/2020/04/20/rabbitmq-gets-an-ha-upgrade&lt;/a&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 6단계: 트랜잭션&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 트랜잭션 모드를 제공한다. 발행자가 &lt;b&gt;`txSelect()`&lt;/b&gt; 를 호출한 후 여러 메시지를 보내고, 마지막에 &lt;b&gt;`txCommit()`&lt;/b&gt;을 호출하면 모두 한 번에 처리된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중간에 문제가 발생하면 &lt;b&gt;`toRollback()`&lt;/b&gt;을 통해 메시지를 무효화할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 성능이 매우 나쁘다고 할 수 있다. RabbitMQ 공식 문서에서도 잘 사용하지 말라고 할 정도로 느린 것 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749981506709&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const amqp = require('amqplib');

(async () =&amp;gt; {
  const conn = await amqp.connect('amqp://localhost');
  const channel = await conn.createChannel();

  const queue = 'tx_queue';

  await channel.assertQueue(queue, { durable: true });

  try {
    // 1. 트랜잭션 시작
    await channel.txSelect();
    console.log('Transaction started');

    // 2. 메시지 발행
    channel.sendToQueue(queue, Buffer.from('msg-1'));
    channel.sendToQueue(queue, Buffer.from('msg-2'));
    // 예: 중간에 에러 발생 시뮬레이션
    if (Math.random() &amp;lt; 0.5) {
      throw new Error('Simulated failure during transaction');
    }

    channel.sendToQueue(queue, Buffer.from('msg-3'));

    // 3. 커밋
    await channel.txCommit();
    console.log('Transaction committed');
  } catch (err) {
    // 4. 롤백
    console.error('Transaction failed, rolling back:', err.message);
    await channel.txRollback();
    console.log('Transaction rolled back');
  }

  await channel.close();
  await conn.close();
})();&lt;/code&gt;&lt;/pre&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 7단계: 트랜잭션 HA 큐&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션 모드에 HA 큐를 더하면, 메시지를 한 번에 처리하면서 복제까지 보장하는 최고의 안정성을 얻게 된다.&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;즉, 일반적인 서비스에서는 과도한 수준이 아닐까 싶다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;### 8단계: 메시지 디스크에 저장 (Durable Queue + Delivery-Mode:2)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브로커가 갑자기 죽거나, 서비스가 재시작되는 상황에서도 메시지를 살리고 싶다면, &lt;b&gt;메시지를 디스크에 저장&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;b&gt;큐는 `durable`, 메시지는 `deliveryMode:2(Persistent Message)` 로 설정&lt;/b&gt;한다. &lt;u&gt;이 두 설정이 모두 되어야 메시지는 완전히 디스크에 저장&lt;/u&gt;된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&lt;b&gt;&quot;소비자&quot;가 메시지를 수신 후, 확인(ACK)을 보내면 디스크의 메시지가 삭제&lt;/b&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;물론 디스크 I/O 가 추가되므로 성능은 떨어진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메시지의 양이 과다하면 &lt;b&gt;운영체제의 디스크 I/O 작업에서 팬딩이 걸릴 수 있기 때문에 하드웨어 성능을 잘 확인&lt;/b&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;&quot;발행자 확인&quot; 방식과 함께 사용한다면 신뢰성을 좀 더 높일 수 있다고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메시지를 디스크에 저장하는 과정은 비동기적으로 처리된다. 완전한 신뢰성을 위해서는 &quot;발행자 확인&quot; 기능을 함께 사용하며, 브로커가 메시지를 디스크에 안전하게 기록한 후에만 발행자에게 완료(ACK)를 보내므로, 발행자는 메시지의 안전한 저장을 확실히 알 수 있다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;이처럼 RabbitMQ는 다양한 단계의 메시지 보장 수단을 제공하는 것으로 보인다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;우리 서비스에 필요한 최소한의 안정성과 감당 가능한 성능 저하의 군형점, 그 '골디락스 존'을 찾는 것이 진짜 중요하고 이 부분에 대해 많은 고민과 테스트를 해야 할 것 같다.&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서: &lt;b&gt;RabbitMQ in Depth / &lt;a style=&quot;color: #000000; text-align: start;&quot; href=&quot;https://search.kyobobook.co.kr/search?keyword=%EA%B0%9C%EB%B9%88%20%EB%A1%9C%EC%9D%B4&amp;amp;chrc=%EA%B0%9C%EB%B9%88%20%EB%A1%9C%EC%9D%B4&quot;&gt;개빈 로이&lt;/a&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;저자(글) &amp;middot;&lt;/span&gt;&lt;a style=&quot;color: #000000; text-align: start;&quot; href=&quot;https://search.kyobobook.co.kr/search?keyword=%ED%99%8D%EC%98%81%ED%83%9D&amp;amp;chrc=%ED%99%8D%EC%98%81%ED%83%9D&quot;&gt;홍영택&lt;/a&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;번역 / 4장&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>Backend/RabbitMQ</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/99</guid>
      <comments>https://jangbageum.tistory.com/99#entry99comment</comments>
      <pubDate>Sat, 14 Jun 2025 21:26:34 +0900</pubDate>
    </item>
    <item>
      <title>[Linux] iptables 의 구조 및 사용 방법</title>
      <link>https://jangbageum.tistory.com/98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;홈서버를 구축하면서 가장 고민이 많았던 부분 중 하나는 네트워크 구성이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 거주하고 있는 건물은 개별 공인 IP를 제공하지 않는 구조였기 때문에, 홈서버를 외부에 직접 노출시키는 것이 사실상 불가능했다. 따라서 외부에서 홈서버에 접근하려면 네트워크를 우회할 수 있는 방법이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 클라우드 컴퓨팅 서비스의 VM을 중간에 두고 네트워크 터널링을 구성했다. 홈서버와 클라우드 VM 사이에 WireGuard 기반의 VPN 터널을 구축한 뒤, 외부 요청은 VM이 받고 이를 홈서버로 전달하는 방식으로 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 중요한 역할을 한 것이 바로 iptables를 이용한 포트포워딩 규칙 설정 있다. 클라우드 VM에서 특정 포트로 들어오는 트래픽을 VPN을 통해 홈서버의 내부 IP로 전달하도록 설정함으로써, 공인 IP 없이도 외부에서 안전하게 홈서버로 접근할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 이 iptables 기반 포트포워딩 설정에 대해 간단히 소개하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;764&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XRqAQ/btsOouEUT82/Ui68WrKTii5nwye2VJo4K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XRqAQ/btsOouEUT82/Ui68WrKTii5nwye2VJo4K1/img.png&quot; data-alt=&quot;https://ko.wikipedia.org/wiki/%EB%B0%A9%ED%99%94%EB%B2%BD_%28%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9%29&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XRqAQ/btsOouEUT82/Ui68WrKTii5nwye2VJo4K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXRqAQ%2FbtsOouEUT82%2FUi68WrKTii5nwye2VJo4K1%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;764&quot; height=&quot;420&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://ko.wikipedia.org/wiki/%EB%B0%A9%ED%99%94%EB%B2%BD_%28%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9%29&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&amp;nbsp;iptables란?&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp;iptables&lt;/b&gt;는 리눅스에서 &lt;b&gt;패킷 필터링과 네트워크 트래픽제어를 담당&lt;/b&gt;하는 명령어 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방화벽 기능을 제공하며, &lt;b&gt;들어오는(incoming), 나가는(outgoing), 포워딩(forwarding)되는 패킷을 원하는 데로 필터링&lt;/b&gt;할 수 있도록 도와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;리눅스 서버를 운영하다 보면 보안이 중요한 시점이 필수적으로 올 수 있다 생각된다. 웹 서버는 외부 HTTP만 받고 DB는 내부 통신만 허용하고 싶다거나, 특정 IP에서만 SSH 접속을 허용하고 싶을 때 iptables를 사용하면 유연한 설정을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&amp;nbsp;iptables 사용되는 경우&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 운영 시 iptables를 사용하는 주요 목적은 다음과 같다.&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; SSH brute force 공격 차단, 포트 스캐닝 탐지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;트래픽 제한:&lt;/b&gt; Dos 방지를 위한 rate limiting&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;네트워크 주소 변환(NAT):&lt;/b&gt; 내부망에서 외부망으로 접속 시 IP 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;iptables의 구조&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iptables는 &lt;b&gt;체인(chain), 테이블(table), 규칙(rule)&lt;/b&gt;으로 구성된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테이블 (Table)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iptables에는 여러 가지 테이블이 있지만 일반적으로 아래 3가지를 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;filter:&lt;/b&gt; 기본 테이블, 패킷 필터링 용도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;nat:&lt;/b&gt; 주소 변환(Network Address Translation) 용도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;mangle:&lt;/b&gt; 패킷을 수정할 때 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;체인 (Chain)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 안에는 여러 체인이 존재하며, 흐름에 따라 패킷이 체인을 통과한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;INPUT:&lt;/b&gt; 로컬 시스템으로 들어오는 패킷&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;OUTPUT:&lt;/b&gt; 로컬 시스템에서 나가는 패킷&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;FORWARD:&lt;/b&gt; 라우팅 되는 패킷 (로컬에서 처리하지 않고 통과하는 경우)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정책과 규칙 (Policy &amp;amp; Rules)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 체인에는 기본 정책이 있으며, 사용자 정의 규칙을 추가해서 동작을 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&amp;nbsp;기본 사용법&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 현재 설정 보기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1748965485678&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo iptables -L -n -v&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;-L&quot;&lt;/b&gt;: 규 리스트 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;-n&quot;&lt;/b&gt;: IP를 숫자로 출력 (DNS 조회 방지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;-v&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;pre id=&quot;code_1748966168128&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Chain INPUT (policy DROP 123 packets, 10456 bytes)
 pkts bytes target     prot opt in     out     source               destination         
   10   800 ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0           
  100 8500 ACCEPT     tcp  --  eth0   *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
   20  1600 ACCEPT     tcp  --  eth0   *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80

Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 200 packets, 15000 bytes)
 pkts bytes target     prot opt in     out     source               destination&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Chain INPUT:&lt;/b&gt; 들어오는 패킷에 대한 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본 정책: DROP (허용되지 않은 패킷은 차단)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- lo 인터페이스(루프백)는 모두 용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- TCP 22번 포트(SSH)는 모두 허용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- TCP 80번 포트(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;&lt;b&gt;Chain FORWARD:&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;Chain OUTPUT:&lt;/b&gt; 나가는 패킷에 대한 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 기본 정책: ACCEPT (모두 허용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기타 참고 내용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;pkts, bytes:&lt;/b&gt; 각 룰에 의해 처리된 패킷 수와 바이트 수를 나타냄. 실시간 트래픽 확인에 유용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;policy:&lt;/b&gt; 각 체인의 기본 동작을 보여줌 (e.g. DROP, ACCEPT 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;target:&lt;/b&gt; 해당 룰의 동작 (e.g. DROP, ACCEPT 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;dpt: destination&lt;/b&gt;&amp;nbsp;port (목적지 포트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 규칙 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 포트를 허용 (e.g. 80번 HTTP 포트)&lt;/p&gt;
&lt;pre id=&quot;code_1748966509618&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT&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;특정 IP만 SSH 접속 허용&lt;/p&gt;
&lt;pre id=&quot;code_1748966542201&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo iptables -A INPUT -p tcp --dport 22 -s 192.168.1.10 -j ACCEPT
$ sudo iptables -A INPUT -p tcp --dport 22 -j DROP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 기본 정책 설정 (디폴트 정책 거부)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1748966577775&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo iptables -P INPUT DROP
$ sudo iptables -P FORWARD DROP
$ sudo iptables -P OUTPUT ACCEPT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 규칙 삭제&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1748966607354&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo iptables -D INPUT -p tcp --dport 80 -j ACCEPT&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 모든 규칙 초기화&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1748966629874&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo iptables -F&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;규칙 저장 및 복원&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;리눅스는 재부팅하면 iptables 설정이 초기화된다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 기반 시스템용 ufw 등의 iptables 프런트 엔드는 자동으로 규칙을 저장하며 재부팅 후에도 설정이 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 iptables는 초기화되기 때문에 저장 및 복구를 위한 별도의 세팅을 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Ubuntu (iptables-persistent 패키지 사용)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동으로 설정파일 백업 후 부팅 시 로드하는 방식으로 설정이 가능하나 가장 흔하게 &lt;b&gt;iptables-persistent&lt;/b&gt; 패키지를 이용하여 저장 및 복구를 하는 듯하다.&lt;/p&gt;
&lt;pre id=&quot;code_1748967433900&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo apt install iptables-persistent
$ sudo netfilter-persistent save
$ sudo netfilter-persistent reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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는 처음 보면 다소 어렵게 느껴질 수 있지만, 원리를 이해하고 자주 쓰이는 패턴만 익혀두면 강력하고 유연한 네트워크 제어 도구가 될 것 같다. 간단한 방화벽 설정은 firewalld, 혹은 ufw와 같이 편리한 툴을 사용하면 되지만 좀 더 유연한 네트워크 세팅이 필요하다면 iptables는 무조건 접할 일이 있을 것 같다. 이러하여 리눅스 서버를 운영한다면 반드시 알아두어야 하는 부분이지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/Iptables&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.wikipedia.org/wiki/Iptables&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;</description>
      <category>Backend/OS</category>
      <category>iptables</category>
      <category>Linux</category>
      <category>network</category>
      <category>OS</category>
      <category>네트워크</category>
      <category>리눅스</category>
      <category>리눅스명렬어</category>
      <category>포트포워딩</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/98</guid>
      <comments>https://jangbageum.tistory.com/98#entry98comment</comments>
      <pubDate>Wed, 4 Jun 2025 01:41:31 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처 설계] 헥사고날 아키텍처 찍먹 후기</title>
      <link>https://jangbageum.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;썸네일하고 본 글의 주제는 관련이 없습니다. &lt;span style=&quot;background-color: #ffffff; text-align: center;&quot;&gt;'◡'✿&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&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;span style=&quot;color: #9d9d9d;&quot;&gt;Layerd Architecture&lt;/span&gt;를 많이 사용해 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 &lt;b&gt;MSA&lt;/b&gt; &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;Microservice Architecture&lt;/span&gt;를 공부하면서 접한 &lt;b&gt;헥사고날 아키텍처&lt;/b&gt; &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;Hexagonal Architecture&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;color: #1f1f1f; text-align: start;&quot;&gt;&amp;nbsp;1. 헥사고날 아키텍처란 &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #9d9d9d; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Hexagonal Architecture&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;&lt;b&gt;헥사고날 아키텍처&lt;/b&gt;는 Alistair Cockburn(애자일 방법론을 제시한 인물 중 1인)이 제한한 아키텍처 패턴이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&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;b&gt;애플리케이션의 핵심 도메인을 외부 세계로부터 격리시키는 것이 핵심&lt;/b&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;u&gt;데이터베이스, 메시징 시스템, HTTP 요청, 콘솔 입력, 파일 I/O 등&lt;/u&gt;이 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법론의 구조는 크게 중심을 담당하는 도메인이 존재하고, 이를 둘러싼 &lt;b&gt;Port와 Adapter를 통해 외부 시스템과 통신&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;&lt;b&gt;육각형 인간&lt;/b&gt;&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;b&gt;도메인, Port, Adapter 등&lt;/b&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;blob&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAe5AY/btsNdlhmNPE/u170QCTTmoQd1SkPt3IpA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAe5AY/btsNdlhmNPE/u170QCTTmoQd1SkPt3IpA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAe5AY/btsNdlhmNPE/u170QCTTmoQd1SkPt3IpA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAe5AY%2FbtsNdlhmNPE%2Fu170QCTTmoQd1SkPt3IpA0%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;956&quot; height=&quot;470&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;2. 실무 적용 사례&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왜 헥사고날 아키텍처를 고민하게 되었을까?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 자사 서비스 플랫폼의 API Gateway를 전담하여 개발을 했었고 현재도 다양한 앤드포인트를 붙이는 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 시작에는 단순한 구조를 가진 레이어드 아키텍처로 구성하여 어렵지 않게 단순한 구조를 가지고 개발을 하였다. 단순했던 서비스가 SaaS형 플랫폼으로 발전하면서 요구사항이 많아지고 다양한 엔드폰이트가 생기면서 말단을 관리하기 복잡해졌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;요구사항이 늘어남에 따라, 요청을 트리거 하는 방식과 관리에 대한 고민이 필요했다.&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;요청을 트리거하는 방식은 그나마 변화가 적었지만 외부 의존성의 경우 앤드포인트와 전문이 바뀌기 다반사이고 프로토콜 자체가 바뀌는 경우도 종종 발생하여 서비스 로직과의 거리를 둘 필요가 있었다.&lt;/li&gt;
&lt;li data-ke-style=&quot;style3&quot;&gt;레이어 간의 결합도가 강해 테스트의 어려움이 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서, &lt;u&gt;외부 연결 점과의 영향을 받지 않고도 도메인 로직을 보호할 수 있는 구조&lt;/u&gt;가 필요하다 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러하여 최근 프로젝트에서 알림 전송 시스템을 구축하면서 헥사고날 아키텍처를 적용해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항의 일부는 아래와 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;외부 API에서 주기적으로 특정 데이터 목록을 조회한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;조회된 데이터 기반으로 주기적으로 외부 API에 알림을 전송한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;애플리케이션 내부 데이터를 매니징 할 수 있도록 자체 API를 제공한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;패키지 구조에는 정답이 없었다.&lt;/b&gt; 단, 참고한 패키지 구조들에는 &lt;b&gt;공통적인 핵심&lt;/b&gt;을 담고 있었다.&lt;/p&gt;
&lt;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;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 구성간의 소통은 인터페이스로 한다. &lt;/b&gt;(의존성 역전)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 의존성 방향을 내부로만 향하게 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 도메인은 독립 적어야 되며 비즈니스 로직은 엔티티에서 구현한다.&lt;/b&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;pre id=&quot;code_1744030713317&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src
├── adapters
│   ├── in-bound
│   │   ├── http
│   │   │   ├── healthcheck.adapter.dto.ts
│   │   │   └── healthcheck.adapter.ts
│   │   └── scheduler
│   │       └── scheduler.adapter.ts
│   └── out-bound
│       ├── a-http-client
│       │   ├── a-http-client.adapter.dto.ts
│       │   └── a-http-client.adapter.ts
│       ├── b-http-client
│       │   ├── b-http-client.adapter.dto.ts
│       │   └── b-http-client.adapter.ts
│       ├── a-repository
│       │   └── a-repository.adapter.ts
│       ├── logger
│       └── environment
├── domains
│   ├── entities
│   │   ├── a.entity.ts
│   │   └── b.entity.ts
│   └── ports
│       ├── a-fetcher.port.ts
│       ├── b-fetcher.port.ts
│       ├── logger.port.ts
│       ├── config.port.ts
│       └── ...
├── services
│   ├── bootstrap.service.ts
│   ├── syncA.service.ts
│   └── ...
├── common
├── main.ts&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;size18&quot;&gt;&lt;b&gt;[ Domains ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 도메인 엔티티와 외부의 소통하기 위한 &lt;b&gt;Port 인터페이스&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;[ Adapter ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 실제 외부 시스템들과 연결하는 부분들로 &lt;b&gt;Port의 구현체&lt;/b&gt;들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;in-bound&lt;/b&gt;: 애플리케이션이 동작할 수 있도록 이벤트가 접근하는 곳이다. (Primary / Driving Adapter라고도 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;out-bound&lt;/b&gt;: 애플리케이션이 의존하는 외부이며 이벤트를 내보내는 곳이다. (Secondary / Driven Adapter라고도 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;Infrastructure&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;[ Services ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Port를 받아서 &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;Application&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;[ main.ts / Bootstrap ]&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;- Port 인터페이스에 실제 Adapter를 주입하고 서비스를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 Port를 어디에 둬야 할지 정말 고민이 많았다. 여기서 내가 느낀 헥사고날 아키텍처의 핵심을 다시 생각해 보니 해답을 찾는데 어렵지 않았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;본 아키텍처의 목표는 도메인의 독립성! -&amp;gt; 외부 시스템과 의존하지 않도록 하는 것!&lt;br /&gt;그렇다면 도메인에서 외부와 통신이 필요할 때 정의하는 것이 &quot;Port&quot; 인터페이스인 것이다.&lt;br /&gt;그러니 &quot;Dmains&quot; 폴더에 포함.&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;&lt;b&gt;Infrastructure ?&lt;/b&gt;&quot; 혹은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&quot;&lt;b&gt;Application ?&lt;/b&gt;&quot;&lt;/span&gt; 등 레이어드 아키텍처로 작업을 많이 해본 나에게는 이질감이 느껴졌다. 그래서 나는 좀 더 명료하고 축소적인 &quot;&lt;b&gt;Adapter&lt;/b&gt;&quot;와 &quot;&lt;b&gt;Service&lt;/b&gt;&quot;라는 폴더 명을 사용하였다. (주관적인 부분)&lt;br /&gt;&lt;br /&gt;아, 그리고 &lt;b&gt;UseCase&lt;/b&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;(UseCase를 간단하게 설명하자면, 도메인의 기능을 사용해 사용자의 특정 요구나 시나리오를 처리하는 비즈니스 로직의 실행 단위라고 한다. Port와 동일하게 인터페이스로 작성하여 사용한다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;3. 레이어드 아키텍처와의 차이점&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;전통적인 레이어드 아키텍처&lt;/b&gt;는 흔히 다음과 같은 구조를 갖는다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; DB&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층이 아래 계층에 의존하며, 도메인 로직은 보통 Service에 섞여 있는 경우가 많다.&lt;/p&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Controller &amp;larr; Port (interface) &amp;rarr; Domain &amp;larr; &amp;nbsp;Port (interface) &amp;rarr; Repository&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 내가 헥사고날 아키텍처를 실무에 적용하면서 느낀 가장 큰 차이는 &quot;&lt;b&gt;의존성은 어디로 향하는가&lt;/b&gt;&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;: 각 계층 간의 의존성이 강하다. (외부 계층인 DB, 프레임워크 등에 강하게 의존한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;b&gt;헥사고날 아키텍처&lt;/b&gt;: 도메인은 외부를 모른다. 즉, 뭐로 어떻게 만들든 Port만 부함 하면 되고 외부가 도메인을 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&amp;nbsp;4. 내가 느낀 장점&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&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;- Mock Adapter 만 만들어서 UseCase 단위로 테스트를 구성하면 된다.&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;- 난 이 부분이 가장 마음에 들었다. &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;size18&quot;&gt;&lt;b&gt;[ 동작을 위한 트리거 방식이 우연하다. ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이 부분도 마음에 들었다. 현재 내 프로젝트에서는 setInterval을 사용하여 만들었지만, 이는 메시지 소비 방식, REST API 요청 방식 등으로 도메인에 영향이 없이 Adapter만 교체하면 된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&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 Gateway를 구현할 때 했던 고민들의 많은 부분들을 헥사고날 아키텍처가 해결해주고 있었고 이상인지 아닌지 맛보고 싶다는 생각이 들었었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165581&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=165581&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/mehdihadeli/awesome-software-architecture/blob/main/docs/hexagonal-architecture.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/mehdihadeli/awesome-software-architecture/blob/main/docs/hexagonal-architecture.md&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)&lt;/a&gt;&lt;/p&gt;</description>
      <category>Backend/개발 방법론 &amp;amp; 디자인 패턴</category>
      <category>ddd</category>
      <category>hexagonal architecture</category>
      <category>layerd architecture</category>
      <category>MicroService Architecture</category>
      <category>MSA</category>
      <category>레이어드 아키텍처</category>
      <category>헥사고날 아키텍처</category>
      <author>장바금</author>
      <guid isPermaLink="true">https://jangbageum.tistory.com/96</guid>
      <comments>https://jangbageum.tistory.com/96#entry96comment</comments>
      <pubDate>Mon, 7 Apr 2025 23:50:07 +0900</pubDate>
    </item>
  </channel>
</rss>