<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>트리어스</title>
    <link>https://leestana01.tistory.com/</link>
    <description>백엔드/인프라 개발 블로그
https://github.com/leestana01
leestana01@naver.com

적극 구직중</description>
    <language>ko</language>
    <pubDate>Sat, 27 Jun 2026 13:41:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>leestana01</managingEditor>
    <image>
      <title>트리어스</title>
      <url>https://tistory1.daumcdn.net/tistory/8517098/attach/ccd070073c0c4391b29c7f67fb28b45d</url>
      <link>https://leestana01.tistory.com</link>
    </image>
    <item>
      <title>JAVA의 자료구조를 톺아보자</title>
      <link>https://leestana01.tistory.com/30</link>
      <description>&lt;h1&gt;Java Collection 톺아보기&lt;/h1&gt;
&lt;h2&gt;List&lt;/h2&gt;
&lt;h3&gt;ArrayList를 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ArrayList&lt;/code&gt;는 이름에 &lt;code&gt;List&lt;/code&gt;가 들어가지만, 본질적으로는 &lt;strong&gt;배열 기반 자료구조&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;배열 공간이 꽉 찬다면?&lt;/strong&gt;&lt;br&gt;더 큰 배열을 새로 만들고, 기존 데이터를 새 배열로 복사한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;LinkedList를 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;LinkedList&lt;/code&gt;는 &lt;code&gt;prev&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt; 형태의 참조로 노드 객체를 연결하는 자료구조이다.&lt;/p&gt;
&lt;p&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;중간 삽입/삭제가 빠르니 무조건 좋을까?&lt;/strong&gt;&lt;br&gt;해당 노드를 이미 알고 있다면 빠르다.&lt;br&gt;하지만 &lt;code&gt;index&lt;/code&gt; 기준으로 삽입/삭제를 한다면, 결국 해당 위치까지 찾아가야 하므로 느리다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;Map&lt;/h2&gt;
&lt;h3&gt;HashMap을 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HashMap&lt;/code&gt;의 기본적인 본질은 &lt;strong&gt;배열 + 해시 + 충돌 처리&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;내부에는 다음과 같은 배열이 존재한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Node&amp;lt;K, V&amp;gt;[] table;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 배열의 각 칸을 &lt;strong&gt;bucket&lt;/strong&gt;이라고 부른다.&lt;/p&gt;
&lt;p&gt;값이 저장되는 흐름은 대략 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;key → hash → index → bucket&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 주어진 &lt;code&gt;key&lt;/code&gt;에 대해 해시를 만들고, 그 해시를 배열 인덱스로 변환한 뒤, 해당 bucket에 데이터를 저장한다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;그러나 서로 다른 key가 같은 bucket에 들어가는 &lt;strong&gt;해시 충돌&lt;/strong&gt;이 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;Java의 &lt;code&gt;HashMap&lt;/code&gt;은 기본적으로 충돌을 &lt;code&gt;LinkedList&lt;/code&gt;로 처리한다.&lt;br&gt;다만, 한 bucket에 노드가 너무 많아지면 &lt;code&gt;Red-Black Tree&lt;/code&gt; 구조로 바뀐다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;충돌 적음 → LinkedList
충돌 많음 → Red-Black Tree&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또한 &lt;code&gt;HashMap&lt;/code&gt;의 배열 크기는 고정되어 있지 않다.&lt;br&gt;일정 비율 이상 데이터가 차면 배열 크기를 늘리는 &lt;strong&gt;resize&lt;/strong&gt;가 발생한다.&lt;/p&gt;
&lt;p&gt;기본 &lt;code&gt;load factor&lt;/code&gt;는 &lt;code&gt;0.75&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;resize가 발생하면 배열 크기가 달라지므로, 기존 key들의 index 계산 결과도 바뀔 수 있다.&lt;br&gt;따라서 기존 노드들을 새 배열에 다시 배치하는 &lt;strong&gt;rehash&lt;/strong&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;HashMap의 성능을 좌우하는 것은?&lt;/strong&gt;&lt;br&gt;&lt;code&gt;hashCode()&lt;/code&gt;가 핵심이다.&lt;br&gt;&lt;code&gt;hashCode()&lt;/code&gt;가 엉망이면 모든 key가 같은 bucket에 몰릴 수 있고, 이 경우 사실상 &lt;code&gt;LinkedList&lt;/code&gt;처럼 동작하게 되어 성능이 떨어진다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;HashSet을 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HashSet&lt;/code&gt;은 내부적으로 &lt;code&gt;HashMap&lt;/code&gt;을 사용해 구현된다.&lt;/p&gt;
&lt;p&gt;다만, 저장하려는 값을 &lt;code&gt;HashMap&lt;/code&gt;의 key로 넣고, value에는 의미 없는 더미 값을 넣는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HashSet의 값 → HashMap의 key
더미 객체 → HashMap의 value&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;TreeMap을 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HashMap&lt;/code&gt;은 정렬을 보장하지 않는다.&lt;/p&gt;
&lt;p&gt;반면 &lt;code&gt;TreeMap&lt;/code&gt;은 key를 기준으로 항상 정렬된 상태를 유지한다.&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;정렬을 보장하지 않는다는 뜻은?&lt;/strong&gt;&lt;br&gt;예를 들어 &lt;code&gt;c&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;를 &lt;code&gt;HashMap&lt;/code&gt;에 넣었다고 해서 순회 결과가 &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;라는 보장은 없다.&lt;br&gt;하지만 &lt;code&gt;TreeMap&lt;/code&gt;은 key 기준 정렬을 유지하므로 &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt; 순서를 보장한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;&lt;code&gt;TreeMap&lt;/code&gt;은 내부적으로 &lt;code&gt;Red-Black Tree&lt;/code&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;왜 Red-Black Tree일까?&lt;/strong&gt;&lt;br&gt;일반적인 이진 탐색 트리, 즉 BST는 데이터가 &lt;code&gt;1 → 2 → 3 → 4 → 5&lt;/code&gt;처럼 들어오면 한쪽으로 편향될 수 있다.&lt;br&gt;이 경우 트리라기보다 사실상 연결 리스트처럼 동작하게 되고, 탐색 성능이 &lt;code&gt;O(n)&lt;/code&gt;까지 떨어질 수 있다.&lt;br&gt;그래서 &lt;code&gt;TreeMap&lt;/code&gt;은 자가 균형 이진 탐색 트리인 &lt;code&gt;Red-Black Tree&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;p&gt;&lt;code&gt;TreeMap&lt;/code&gt;은 평균적인 조회 성능만 보면 &lt;code&gt;HashMap&lt;/code&gt;보다 느리다.&lt;br&gt;하지만 key의 정렬이 필요하다면 &lt;code&gt;TreeMap&lt;/code&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;언제 사용하면 좋을까?&lt;/strong&gt;&lt;br&gt;가장 작은 key가 필요할 때&lt;br&gt;가장 큰 key가 필요할 때&lt;br&gt;특정 범위의 key가 필요할 때&lt;br&gt;현재 key보다 큰 key가 필요할 때&lt;br&gt;현재 key보다 작은 key가 필요할 때&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h3&gt;TreeSet을 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TreeSet&lt;/code&gt;도 &lt;code&gt;HashSet&lt;/code&gt;처럼 내부적으로 Map을 사용한다.&lt;/p&gt;
&lt;p&gt;다만 &lt;code&gt;TreeSet&lt;/code&gt;은 &lt;code&gt;HashMap&lt;/code&gt;이 아니라 &lt;code&gt;TreeMap&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;p&gt;저장하려는 값을 &lt;code&gt;TreeMap&lt;/code&gt;의 key로 넣고, value에는 의미 없는 더미 값을 넣는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TreeSet의 값 → TreeMap의 key
더미 객체 → TreeMap의 value&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Queue / Stack / Deque&lt;/h2&gt;
&lt;h3&gt;PriorityQueue를 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;PriorityQueue&lt;/code&gt;는 이름에 Queue가 들어가지만, 일반적인 선입선출 큐라기보다는 &lt;strong&gt;Binary Heap 기반 자료구조&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;내부적으로는 배열을 사용한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Object[] queue;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기본적으로 루트에 가장 작은 값이 위치한다.&lt;/p&gt;
&lt;p&gt;따라서 최소값을 빠르게 꺼내야 할 때 유용하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;ArrayDeque를 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ArrayDeque&lt;/code&gt;는 &lt;code&gt;head&lt;/code&gt;와 &lt;code&gt;tail&lt;/code&gt;을 이용한 &lt;strong&gt;원형 배열 기반 Deque&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;앞쪽과 뒤쪽 양방향에서 삽입과 삭제가 가능하다.&lt;/p&gt;
&lt;p&gt;Java에서는 오래된 &lt;code&gt;Stack&lt;/code&gt; 클래스 대신 &lt;code&gt;ArrayDeque&lt;/code&gt;를 스택처럼 사용하는 경우가 많다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Stack을 톺아보자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Stack&lt;/code&gt;은 내부적으로 &lt;code&gt;Vector&lt;/code&gt;를 상속한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Vector&lt;/code&gt;는 동기화된 동적 배열이다.&lt;/p&gt;
&lt;p&gt;하지만 요즘 Java에서는 &lt;code&gt;Stack&lt;/code&gt;과 &lt;code&gt;Vector&lt;/code&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: 'Noto Serif KR';&quot;&gt;&lt;p&gt;  &lt;strong&gt;왜 Vector를 잘 사용하지 않을까?&lt;/strong&gt;&lt;br&gt;단일 스레드 환경에서는 동기화가 불필요한 비용이 된다.&lt;br&gt;멀티 스레드 환경에서는 &lt;code&gt;Vector&lt;/code&gt;보다 더 세분화되고 목적에 맞는 동시성 컬렉션들이 존재한다.&lt;br&gt;따라서 요즘은 &lt;code&gt;Stack&lt;/code&gt; 대신 &lt;code&gt;ArrayDeque&lt;/code&gt;를 사용하는 것이 더 일반적으로 권장된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>백엔드/SpringBoot, 백엔드심화</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/30</guid>
      <comments>https://leestana01.tistory.com/30#entry30comment</comments>
      <pubDate>Thu, 18 Jun 2026 16:53:55 +0900</pubDate>
    </item>
    <item>
      <title>커스텀 파라미터, 그 원리와 사용방법은?</title>
      <link>https://leestana01.tistory.com/29</link>
      <description>&lt;h1&gt;커스텀 파라미터 구현&lt;/h1&gt;
&lt;p&gt;설명에 앞서, 먼저 간단한 예시 코드를 작성해본다.&lt;/p&gt;
&lt;p&gt;다음과 같은 커스텀 파라미터를 가정한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.PARAMETER)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface CurrentUserId {  
}  
@CurrentUserId 자체는 파라미터 전용 런타임 Annotation입니다.  &lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이는 다음 커스텀 Resolver가 처리한다.  &lt;/p&gt;
&lt;p&gt;Resolver에 필요한 supportsParameter와 resolveArgument를 Override해주었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class CurrentUserIdArgumentResolver implements HandlerMethodArgumentResolver {  

@Override  
public boolean supportsParameter(MethodParameter parameter) {  
  return parameter.hasParameterAnnotation(CurrentUserId.class)   
    &amp;amp;&amp;amp; Long.class.equals(parameter.getParameterType());  
}  

@Override  
public Long resolveArgument(...머시깽이...) {  
  ...저시깽이...  
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;마지막으로 설정을 위해 WebConfig에 다음을 추가했다고 가정한다. &lt;/p&gt;
&lt;p&gt;Resolver 목록에 방금 제작한 커스텀 Resolver를 추가한 것이다.&lt;/p&gt;
&lt;p&gt;이로써 방금 생성한 어노테이션을 활용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override public void addArgumentResolvers(List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {   
  resolvers.add(new CurrentUserIdArgumentResolver());   
}&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;커스텀 파라미터는 어떻게 인식되는가?&lt;/h1&gt;
&lt;p&gt;이제 컨트롤러에서 다음으로 호출하는 경우를 보자.&lt;/p&gt;
&lt;h2&gt;Controller의 파라미터부터&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@PostMapping(&amp;quot;/api/participate&amp;quot;)  
ApiResponse&amp;lt;?&amp;gt; participate(@CurrentUserId Long userId)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Spring MVC는 컨트롤러 호출 직전에 각 파라미터를 하나씩 해석한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@CurrentUserId Long userId&lt;/code&gt;를 만나면&lt;br&gt;resolver 목록을 돌면서 supportsParameter()를 호출하고,&lt;br&gt;현재 resolver가 true를 반환한다.&lt;br&gt;그 다음 resolveArgument()가 실행된다.&lt;/p&gt;
&lt;h2&gt;resolver 흐름&lt;/h2&gt;
&lt;p&gt;상기 예시의 HandlerMethodArgumentResolver를 다시 보자.&lt;/p&gt;
&lt;p&gt;public class CurrentUserIdArgumentResolver implements HandlerMethodArgumentResolver {  &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Override  
public boolean supportsParameter(MethodParameter parameter) {  
  return parameter.hasParameterAnnotation(CurrentUserId.class)   
    &amp;amp;&amp;amp; Long.class.equals(parameter.getParameterType());  
}  

@Override  
public Long resolveArgument(...머시깽이...) {  
  ...저시깽이...  
}  &lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 Resolver는 @CurrentUserId에 대하여,&lt;br&gt;supportsParameter에 의해 true를 반환한다.&lt;/p&gt;
&lt;p&gt;이후, resolveArgument()는 &amp;quot;저시깽이&amp;quot;의 코드에 따라 Long 값을 반환한다.&lt;/p&gt;
&lt;p&gt;그리고 이 Long 값이 아까 봤던 Controller의 다음 코드에서&lt;br&gt;&lt;code&gt;@CurrentUserId Long userId&lt;/code&gt; 내부에 들어갈 값이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@PostMapping(&amp;quot;/api/participate&amp;quot;)  
ApiResponse&amp;lt;?&amp;gt; participate(@CurrentUserId Long userId)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>백엔드/SpringBoot, 백엔드심화</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/29</guid>
      <comments>https://leestana01.tistory.com/29#entry29comment</comments>
      <pubDate>Thu, 18 Jun 2026 16:48:48 +0900</pubDate>
    </item>
    <item>
      <title>프론트의 CSR과 SSR에 따른 백엔드의 대응 방식 연구</title>
      <link>https://leestana01.tistory.com/26</link>
      <description>&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEpfKA/dJMcabp9mkU/EuC8gN5QFNvqr7FqY6ium1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEpfKA/dJMcabp9mkU/EuC8gN5QFNvqr7FqY6ium1/img.png&quot; data-alt=&quot;본인 허락 받고 인용함&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEpfKA/dJMcabp9mkU/EuC8gN5QFNvqr7FqY6ium1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEpfKA%2FdJMcabp9mkU%2FEuC8gN5QFNvqr7FqY6ium1%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;394&quot; height=&quot;195&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;376&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;최근 프론트 팀원들의 컴포넌트 CSR과 SSR에 대한 열띤 논의가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, CSR과 SSR은 프론트 측의 단순 UI 렌더링만 관련있을까?&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;p data-ke-size=&quot;size16&quot;&gt;이 게시글을 통해 프론트와 백엔드 모두 컴포넌트 및 api 설계에 도움이 되길 바란다.&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 api에 따른 프론트 컴포넌트의 csr과 ssr의 영향을 분석하고자 한다.&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;가설&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 원활히 테스트 할 수 있도록 다음과 같이 범용적인 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;상품 판매&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;시나리오를 가정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;2600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lx4tW/dJMcah4Xr5a/e8Kkr7htRTjvEk4EGJRgD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lx4tW/dJMcah4Xr5a/e8Kkr7htRTjvEk4EGJRgD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lx4tW/dJMcah4Xr5a/e8Kkr7htRTjvEk4EGJRgD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flx4tW%2FdJMcah4Xr5a%2Fe8Kkr7htRTjvEk4EGJRgD0%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;544&quot; height=&quot;855&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;2600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 가장 직관적으로 크롬 디버깅 툴로 직접 조사해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Full SSR과 Full CSR로 SSR과 CSR을 분석하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 백엔드 V1과 V2를 통해 백엔드 api의 영향을 분석한다.&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: 25%;&quot;&gt;Full SSR&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;br /&gt;&lt;/span&gt;+ 백엔드 V1(단일응답)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;Full CSR&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;br /&gt;&lt;/span&gt;+ 백엔드 V1(단일응답)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;Hybrid &lt;br /&gt;+ 백엔드 V1(단일응답)&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;Hybrid &lt;br /&gt;+ 백엔드 V2(분할응답)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHZMGL/dJMcahw7P7z/kvBWzmC6asJuCVwMmhXUkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHZMGL/dJMcahw7P7z/kvBWzmC6asJuCVwMmhXUkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHZMGL/dJMcahw7P7z/kvBWzmC6asJuCVwMmhXUkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHZMGL%2FdJMcahw7P7z%2FkvBWzmC6asJuCVwMmhXUkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;710&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SqpzM/dJMcahql06S/DoRItpdRdFquVEgIIYshl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SqpzM/dJMcahql06S/DoRItpdRdFquVEgIIYshl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SqpzM/dJMcahql06S/DoRItpdRdFquVEgIIYshl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSqpzM%2FdJMcahql06S%2FDoRItpdRdFquVEgIIYshl0%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;756&quot; height=&quot;624&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xpnlc/dJMcahql0mi/CixqOJBGkBns8XMMUIyKO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xpnlc/dJMcahql0mi/CixqOJBGkBns8XMMUIyKO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xpnlc/dJMcahql0mi/CixqOJBGkBns8XMMUIyKO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxpnlc%2FdJMcahql0mi%2FCixqOJBGkBns8XMMUIyKO1%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;752&quot; height=&quot;624&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SIkxy/dJMcadVJFGM/j8VqocSAjxvKKTxUlO2Ck1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SIkxy/dJMcadVJFGM/j8VqocSAjxvKKTxUlO2Ck1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SIkxy/dJMcadVJFGM/j8VqocSAjxvKKTxUlO2Ck1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSIkxy%2FdJMcadVJFGM%2Fj8VqocSAjxvKKTxUlO2Ck1%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;754&quot; height=&quot;720&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQaTs/dJMcaaEMeDY/a5nanBkii6ZSkXHnkUOsV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQaTs/dJMcaaEMeDY/a5nanBkii6ZSkXHnkUOsV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQaTs/dJMcaaEMeDY/a5nanBkii6ZSkXHnkUOsV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQaTs%2FdJMcaaEMeDY%2Fa5nanBkii6ZSkXHnkUOsV0%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;1314&quot; height=&quot;308&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v2I5o/dJMcabjn0un/19Z2UPiNDlNNAgQKciYVhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v2I5o/dJMcabjn0un/19Z2UPiNDlNNAgQKciYVhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v2I5o/dJMcabjn0un/19Z2UPiNDlNNAgQKciYVhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv2I5o%2FdJMcabjn0un%2F19Z2UPiNDlNNAgQKciYVhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;720&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EpzCf/dJMcahql1sj/9YkKtMmL76Q1MHtmVvqd9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EpzCf/dJMcahql1sj/9YkKtMmL76Q1MHtmVvqd9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EpzCf/dJMcahql1sj/9YkKtMmL76Q1MHtmVvqd9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEpzCf%2FdJMcahql1sj%2F9YkKtMmL76Q1MHtmVvqd9k%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;762&quot; height=&quot;714&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;가장 오래걸린 api&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 상세하게 표로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갱신 기준은 Price&amp;amp;Stock 정보를 매번 새롭게 갱신하는 것을 가정한다.&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 15.6977%;&quot;&gt;&lt;b&gt;Full SSR&lt;br /&gt;+백엔드v1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 17.6743%;&quot;&gt;&lt;b&gt;Full CSR&lt;br /&gt;+백엔드v1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.0234%;&quot;&gt;&lt;b&gt;Hybrid&lt;br /&gt;+백엔드v1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.6046%;&quot;&gt;&lt;b&gt;&lt;b&gt;Hybrid&lt;br /&gt;+백엔드v2&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;TTFB&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;SSR 744.84ms&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;Html 로드 110ms&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;API 갱신 711.91ms&lt;br /&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;(+ 갱신시마다 711.91ms)&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;SSR 734.18ms&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;API 갱신 716.21ms&lt;br /&gt;(+ 갱신시마다 716.21ms)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9;&quot;&gt;SSR 73.54ms&lt;br /&gt;API 갱신 305.44ms&lt;br /&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;(+ 갱신시마다 203.83ms)&lt;/span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;설명&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;전체 SSR 로딩&lt;br /&gt;백엔드 API 통합&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;전체 CSR 로딩&lt;br /&gt;백엔드 API 통합&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;SSR + CSR 분리&lt;br /&gt;백엔드 API 통합&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;SSR + CSR 분리&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f9f9f9; text-align: start;&quot;&gt;백엔드 API 분리&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: start;&quot;&gt;API 호출 수&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;1회 (서버)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;1회 (클라이언트)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;2회+ (중복!)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;4회 (분리)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;초기 HTML&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;완전한 콘텐츠&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;빈 껍데기&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;완전한 콘텐츠&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;기본정보만 포함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;실시간 갱신&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;불가&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;가능&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;가능&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;&lt;b&gt;가능&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;데이터 낭비&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;중복 전송&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;캐시 효율&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;불가&lt;/b&gt;&lt;/span&gt; (통합)&lt;/td&gt;
&lt;td style=&quot;width: 17.6743%;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;불가&lt;/b&gt;&lt;/span&gt; (통합)&lt;/td&gt;
&lt;td style=&quot;width: 18.0234%;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;불가&lt;/b&gt;&lt;/span&gt; (통합)&lt;/td&gt;
&lt;td style=&quot;width: 18.6046%;&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;&lt;b&gt;static 캐시&lt;/b&gt;&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 useEffect/useCallback를 통해 로드되는 시점과 이후의 시간을 비교했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vvh6w/dJMcajn5fLl/N2KeGUxAbHNw0KkavSkbJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vvh6w/dJMcajn5fLl/N2KeGUxAbHNw0KkavSkbJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vvh6w/dJMcajn5fLl/N2KeGUxAbHNw0KkavSkbJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvvh6w%2FdJMcajn5fLl%2FN2KeGUxAbHNw0KkavSkbJ0%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;1604&quot; height=&quot;1274&quot; data-origin-width=&quot;1604&quot; data-origin-height=&quot;1274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;분석&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;프론트가 CSR + SSR 구조를 가져가고, 이에 맞춰 백엔드가 api를 설계하면 베스트&lt;/b&gt;&lt;/span&gt;이다.&lt;br /&gt;그러나 &lt;u&gt;하나라도 어긋나면 성능이 극도로 저하&lt;/u&gt;된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 프론트/백엔드 소통이 이루어지지 않으면 무용지물이다.&lt;/li&gt;
&lt;li&gt;프론트가 SSR ALL 또는 CSR ALL을 쓰면, 백엔드가 api 설계를 잘해도 의미가 없다.&lt;/li&gt;
&lt;li&gt;프론트가 CSR + SSR을 써도, &lt;br /&gt;백엔드가 어느 포인트에서 api 분리가 필요한지 알지 못한다면 api 응답은 중복되어 성능이 극감할수있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSR이 근소하게 빠르다.&lt;/b&gt;&lt;br /&gt;로컬호스트에서 진행된 만큼, 프로덕션에서는 서버 환경이 가장 큰 예외 변수일 것 같다.&lt;/li&gt;
&lt;li&gt;API 호출 수가 많으면 TCP Connection이 많아져 더 느려질 거라고 생각했는데,&lt;br /&gt;어차피 동시에 비동기 요청하므로 의미 없다는걸 깨달았다. 그냥 서버만 죽어나갈 뿐..&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 게시글은 성능만 언급했다. 그러나 SSR 이 도입될수록 서버 관리는 극악이다.&lt;/p&gt;
&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7U9CL/dJMcaco2rwI/AIVh4KNYcbhKC3gsXqi2Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7U9CL/dJMcaco2rwI/AIVh4KNYcbhKC3gsXqi2Yk/img.png&quot; data-alt=&quot;출처 : https://toss.tech/article/ssr-server&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7U9CL/dJMcaco2rwI/AIVh4KNYcbhKC3gsXqi2Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7U9CL%2FdJMcaco2rwI%2FAIVh4KNYcbhKC3gsXqi2Yk%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;598&quot; height=&quot;286&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://toss.tech/article/ssr-server&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀라웠던 사실은 토스는 코드 상의 최적화를 넘어, 인프라 팀과 협업하며 지표를 분석했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 확인하는 과정이 매우 놀라운데, 꽤나 재밌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 재밌던건 OpenTelemetry 지원안한다고 최신버전에서 hook 빼내서 삽입하고, &lt;br /&gt;Next.js 커스텀서버에서 Express를 제거해 Node 내장 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;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Node.js SSR 서버의 성능을 개선하고 싶다면, 다음의 과정을 한 번 고민해보시면 좋겠습니다.&lt;/b&gt;&lt;br /&gt;1. 현재의 런타임은 어떻게 구성되어 있는가? 네트워크 토폴로지는 어떻게 구성되어 있으며, 어떤 요청이 오가는가?&lt;br /&gt;2. 측정이 필요한 성능은 무엇이 있는가? 측정할 수 있는가?&lt;br /&gt;3. 개선 전후를 확인할 수 있는, 변인이 통제된 재현 환경이 존재하는가?&lt;/blockquote&gt;</description>
      <category>백엔드/SpringBoot, 백엔드심화</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/26</guid>
      <comments>https://leestana01.tistory.com/26#entry26comment</comments>
      <pubDate>Tue, 31 Mar 2026 05:47:03 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 기본부터 극한까지 날먹하기 (Pydantic, Depends 심화)</title>
      <link>https://leestana01.tistory.com/25</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Discriminated Union&lt;/h2&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;Spring에서는&amp;nbsp;@JsonTypeInfo&amp;nbsp;+&amp;nbsp;@JsonSubTypes를 사용해야 하고, 커스텀 디시리얼라이저를 작성해야 하는 경우도 많다.&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;Pydantic에서는 매우 간단하게 처리 가능하다. 다음 코드를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;from pydantic import BaseModel, Field
from typing import Literal, Annotated, Union
from enum import Enum

# 각 결제 수단별 요청 스키마
class CardPaymentRequest(BaseModel):
    method: Literal[&quot;CARD&quot;] = &quot;CARD&quot;
    card_number: str = Field(..., pattern=r&quot;^\d{16}$&quot;)
    expiry_month: int = Field(..., ge=1, le=12)
    expiry_year: int = Field(..., ge=2024, le=2035)
    cvv: str = Field(..., pattern=r&quot;^\d{3,4}$&quot;)
    installment_months: int = Field(default=0, ge=0, le=12)

class BankTransferRequest(BaseModel):
    method: Literal[&quot;BANK_TRANSFER&quot;] = &quot;BANK_TRANSFER&quot;
    bank_code: str = Field(..., pattern=r&quot;^\d{3}$&quot;)
    account_number: str = Field(..., pattern=r&quot;^\d{10,14}$&quot;)
    account_holder: str = Field(..., min_length=2, max_length=20)

class EasyPayRequest(BaseModel):
    method: Literal[&quot;EASY_PAY&quot;] = &quot;EASY_PAY&quot;
    provider: str = Field(..., pattern=&quot;^(TOSS|KAKAO|NAVER)$&quot;)
    token: str

# Discriminated Union &amp;mdash; method 필드를 기준으로 자동 판별
PaymentMethodRequest = Annotated[
    Union[CardPaymentRequest, BankTransferRequest, EasyPayRequest],
    Field(discriminator=&quot;method&quot;),
]

class CreatePaymentRequest(BaseModel):
    amount: int = Field(..., gt=0, le=100_000_000)
    currency: str = Field(default=&quot;KRW&quot;, pattern=&quot;^(KRW|USD|JPY)$&quot;)
    order_id: str = Field(..., min_length=1, max_length=64)
    payment_method: PaymentMethodRequest  # 결제 수단에 따라 다른 스키마 적용
    description: str | None = Field(None, max_length=200)

# 요청 예시:
# {&quot;amount&quot;: 50000, &quot;order_id&quot;: &quot;ORD-001&quot;,
#  &quot;payment_method&quot;: {&quot;method&quot;: &quot;CARD&quot;, &quot;card_number&quot;: &quot;1234567890123456&quot;, ...}}
# &amp;rarr; CardPaymentRequest로 자동 파싱

# {&quot;amount&quot;: 50000, &quot;order_id&quot;: &quot;ORD-002&quot;,
#  &quot;payment_method&quot;: {&quot;method&quot;: &quot;BANK_TRANSFER&quot;, &quot;bank_code&quot;: &quot;004&quot;, ...}}
# &amp;rarr; BankTransferRequest로 자동 파싱&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;응답 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response에서 데이터를 가공해서 필요한 응답을 제공해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간편하게 즉시 연산하여 반환할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;from pydantic import BaseModel, ConfigDict, computed_field
from datetime import datetime

class PaymentResponse(BaseModel):
    &quot;&quot;&quot;결제 응답 &amp;mdash; 민감 정보 마스킹 포함&quot;&quot;&quot;
    model_config = ConfigDict(
        from_attributes=True,     # ORM 객체에서 자동 변환 (= ModelMapper)
        populate_by_name=True,    # 별칭과 원래 이름 모두 허용
    )

    payment_id: UUID
    status: PaymentStatus
    amount: int
    currency: str
    fee: int
    description: str | None
    created_at: datetime
    completed_at: datetime | None

    # Computed Field &amp;mdash; 파생 값 자동 계산
    @computed_field
    @property
    def total_amount(self) -&amp;gt; int:
        &quot;&quot;&quot;수수료 포함 총액&quot;&quot;&quot;
        return self.amount + self.fee

    @computed_field
    @property
    def masked_card_number(self) -&amp;gt; str | None:
        &quot;&quot;&quot;카드번호 마스킹 (핀테크 필수)&quot;&quot;&quot;
        if hasattr(self, &quot;_card_number&quot;) and self._card_number:
            return f&quot;{'*' * 12}{self._card_number[-4:]}&quot;
        return None

# 목록 조회용 간소화 응답
class PaymentSummary(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    payment_id: UUID
    status: PaymentStatus
    amount: int
    created_at: datetime

# 페이지네이션 래퍼 (제네릭)
T = TypeVar(&quot;T&quot;, bound=BaseModel)

class PaginatedResponse(BaseModel, Generic[T]):
    items: list[T]
    total_count: int
    page: int
    size: int

    @computed_field
    @property
    def total_pages(self) -&amp;gt; int:
        return (self.total_count + self.size - 1) // self.size

    @computed_field
    @property
    def has_next(self) -&amp;gt; bool:
        return self.page &amp;lt; self.total_pages&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Custom Serializer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api 응답할 때 dto 수준에서 금액을 바로 포맷팅 해보자.&lt;/p&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;from pydantic import field_serializer, field_validator

class MoneyResponse(BaseModel):
    amount: int           # 내부: 정수 (원 단위)
    currency: str

    @field_serializer(&quot;amount&quot;)
    def serialize_amount(self, value: int, _info) -&amp;gt; str:
        &quot;&quot;&quot;API 응답에서 금액을 포맷팅&quot;&quot;&quot;
        if self.currency == &quot;KRW&quot;:
            return f&quot;{value:,}원&quot;
        return f&quot;{value / 100:.2f}&quot;  # USD 등은 센트 단위

    # 역직렬화 시 (요청 받을 때) 타입 변환
    @field_validator(&quot;amount&quot;, mode=&quot;before&quot;)
    @classmethod
    def parse_amount(cls, v: int | str) -&amp;gt; int:
        if isinstance(v, str):
            return int(v.replace(&quot;,&quot;, &quot;&quot;).replace(&quot;원&quot;, &quot;&quot;))
        return v&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;의존성 주입(DI) 심화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depends()는 &quot;함수를 호출하고, 그 반환값을 파라미터로 주입&quot;하는 것이 전부다.&lt;br /&gt;즉, Spring의 IoC Container와는 근본적으로 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring: 컨테이너가 빈의 생명주기를 관리 (생성, 주입, 소멸)&lt;/li&gt;
&lt;li&gt;FastAPI: Depends()가 호출된 시점에 팩토리 함수를 실행하여 값을 주입&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Depends()의 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청 도착&lt;/li&gt;
&lt;li&gt;FastAPI가 엔드포인트 함수의 파라미터를 분석&lt;/li&gt;
&lt;li&gt;Depends()를 발견하면 해당 함수를 호출&lt;/li&gt;
&lt;li&gt;Depends() 체이닝이 있으면 재귀적으로 해결&lt;/li&gt;
&lt;li&gt;모든 의존성이 해결되면 엔드포인트 함수 호출&lt;/li&gt;
&lt;li&gt;요청 종료 시 generator 기반 의존성의 cleanup 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;yield를 사용하면 cleanup 로직을 정의할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;async def get_db() -&amp;gt; AsyncGenerator[AsyncSession, None]:
    session = async_session_factory()
    try:
        yield session        # &amp;larr; 여기서 값이 주입됨
        await session.commit()
    except Exception:
        await session.rollback()
        raise
    finally:
        await session.close()  # &amp;larr; 요청 종료 시 실행됨 (= @PreDestroy)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Depends() 보일러플레이트 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Depends() 팩토리 함수가 끝없이 늘어나는 경우이다.&lt;br /&gt;10개 서비스에서 동일하게 아래 코드가 반복 된다고 가정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1774871236394&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def get_payment_repo(db: AsyncSession = Depends(get_db)) -&amp;gt; PaymentRepository:
    return PaymentRepository(db)

def get_payment_service(
    repo: PaymentRepository = Depends(get_payment_repo),
) -&amp;gt; PaymentService:
    return PaymentService(repo)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 크게 두 가지 방식으로 해결한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Annotated 타입으로 재사용하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Spring의 @Autowired처럼)&lt;/li&gt;
&lt;li&gt;Class 기반 의존성을 활용하거나 (Spring의 @Component처럼)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Annotated 타입으로 재사용 하는 방법&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from typing import Annotated

DBSession = Annotated[AsyncSession, Depends(get_db)]
AuthUser = Annotated[CurrentUser, Depends(get_current_user)]
PaymentSvc = Annotated[PaymentService, Depends(get_payment_service)]

@router.post(&quot;/payments&quot;)
async def create_payment(
    request: CreatePaymentRequest,
    db: DBSession,              # 깔끔
    user: AuthUser,             # 깔끔
    service: PaymentSvc,        # 깔끔
) -&amp;gt; PaymentResponse:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Class 기반 의존성으로 처리하는 방법 (Spring @Component 처럼)&lt;/h4&gt;
&lt;pre id=&quot;code_1774871377893&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PaymentService:
    &quot;&quot;&quot;
    __init__에 Depends()를 사용하면 클래스 자체를 Depends()에 넣을 수 있다.
    Spring의 생성자 주입과 거의 동일한 형태이다.
    &quot;&quot;&quot;
    def __init__(
        self,
        repo: PaymentRepository = Depends(get_payment_repo),
        fee_calc: FeeCalculator = Depends(get_fee_calculator),
        event_bus: EventBus = Depends(get_event_bus),
    ):
        self.repo = repo
        self.fee_calc = fee_calc
        self.event_bus = event_bus

    async def create_payment(self, request: CreatePaymentRequest) -&amp;gt; Payment:
        ...

# 라우터에서 바로 클래스를 Depends에 전달
@router.post(&quot;/payments&quot;)
async def create_payment(
    request: CreatePaymentRequest,
    service: PaymentService = Depends(),  # &amp;larr; Depends(PaymentService)와 동일
) -&amp;gt; PaymentResponse:
    return await service.create_payment(request)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;싱글톤 의존성 (app.state와 lifespan)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI에서 싱글톤이 필요한 경우들이 있다. 가령, 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 이렇게 나열할 필요도 없이, 당연히 싱글톤으로 작성되어야 하는 설정 정보들이다.)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 엔진 (커넥션 풀)&lt;/li&gt;
&lt;li&gt;Redis 클라이언트&lt;/li&gt;
&lt;li&gt;HTTP 클라이언트 풀&lt;/li&gt;
&lt;li&gt;설정 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 싱글톤 초기화
    app.state.db_engine = create_async_engine(settings.DATABASE_URL)
    app.state.redis = redis.from_url(settings.REDIS_URL)
    app.state.http_client = httpx.AsyncClient(timeout=30.0)

    yield

    # 싱글톤 정리
    await app.state.db_engine.dispose()
    await app.state.redis.close()
    await app.state.http_client.aclose()

app = FastAPI(lifespan=lifespan)

# 싱글톤 접근 패턴
from fastapi import Request

async def get_redis(request: Request) -&amp;gt; redis.Redis:
    return request.app.state.redis

RedisClient = Annotated[redis.Redis, Depends(get_redis)]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;async/await&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;동시성 모델의 근본적 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring을 경험해보지 못한 사람들에게는 미안하지만, 원활한 이해를 위해 Spring과 비교해보자.&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;Spring MVC&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;FastAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Thread Pool (200 스레드)&lt;br /&gt;-&amp;nbsp;Thread-1:&amp;nbsp;요청&amp;nbsp;A&amp;nbsp;처리&amp;nbsp;중&amp;nbsp;(DB&amp;nbsp;대기...&amp;nbsp;3초&amp;nbsp;동안&amp;nbsp;스레드&amp;nbsp;점유)&lt;br /&gt;-&amp;nbsp;Thread-2:&amp;nbsp;요청&amp;nbsp;B&amp;nbsp;처리&amp;nbsp;중&amp;nbsp;(API&amp;nbsp;호출&amp;nbsp;대기...&amp;nbsp;5초&amp;nbsp;동안&amp;nbsp;스레드&amp;nbsp;점유)&lt;br /&gt;-&amp;nbsp;Thread-3:&amp;nbsp;요청&amp;nbsp;C&amp;nbsp;처리&amp;nbsp;중&lt;br /&gt;-&amp;nbsp;...&amp;nbsp;197개&amp;nbsp;남은&amp;nbsp;스레드&lt;br /&gt;&lt;br /&gt;&amp;rarr;&amp;nbsp;동시&amp;nbsp;처리&amp;nbsp;한계&amp;nbsp;=&amp;nbsp;스레드&amp;nbsp;수&amp;nbsp;(200)&lt;br /&gt;&amp;rarr;&amp;nbsp;I/O&amp;nbsp;대기&amp;nbsp;중에도&amp;nbsp;스레드를&amp;nbsp;점유하므로&amp;nbsp;비효율적&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Event Loop (1개) + 코루틴 (무제한)&lt;br /&gt;-&amp;nbsp;코루틴&amp;nbsp;A:&amp;nbsp;DB&amp;nbsp;쿼리&amp;nbsp;await&amp;nbsp;&amp;rarr;&amp;nbsp;이벤트&amp;nbsp;루프에&amp;nbsp;제어권&amp;nbsp;반환&amp;nbsp;&amp;rarr;&amp;nbsp;다른&amp;nbsp;코루틴&amp;nbsp;실행&lt;br /&gt;-&amp;nbsp;코루틴&amp;nbsp;B:&amp;nbsp;API&amp;nbsp;호출&amp;nbsp;await&amp;nbsp;&amp;rarr;&amp;nbsp;이벤트&amp;nbsp;루프에&amp;nbsp;제어권&amp;nbsp;반환&amp;nbsp;&amp;rarr;&amp;nbsp;다른&amp;nbsp;코루틴&amp;nbsp;실행&lt;br /&gt;-&amp;nbsp;코루틴&amp;nbsp;C:&amp;nbsp;처리&amp;nbsp;중&lt;br /&gt;-&amp;nbsp;...&amp;nbsp;수천&amp;nbsp;개의&amp;nbsp;코루틴이&amp;nbsp;하나의&amp;nbsp;스레드에서&amp;nbsp;교대&amp;nbsp;실행&lt;br /&gt;&lt;br /&gt;&amp;rarr;&amp;nbsp;동시&amp;nbsp;처리&amp;nbsp;한계&amp;nbsp;=&amp;nbsp;메모리&amp;nbsp;(사실상&amp;nbsp;무제한)&lt;br /&gt;&amp;rarr;&amp;nbsp;I/O&amp;nbsp;대기&amp;nbsp;중&amp;nbsp;스레드를&amp;nbsp;해제하므로&amp;nbsp;매우&amp;nbsp;효율적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Event Loop 방식 덕분에 소규모 프로젝트에서는 얻는 장점이 매우 크다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 다음의 문제가 동반된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Event Loop가 막히면 전체 서비스가 막힌다는 점&lt;/li&gt;
&lt;li&gt;하나만 async를 써도 전체가 async를 강제로 써야한다는 점 (일부만 비동기 불가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;비동기 동시 실행 패턴&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import asyncio
import httpx

# === 순차 실행 (나쁜 예) ===
async def enrich_payment_sequential(payment_id: UUID) -&amp;gt; dict:
    &quot;&quot;&quot;3개 API를 순서대로 호출 &amp;rarr; 총 3초&quot;&quot;&quot;
    fraud = await fraud_client.check(payment_id)        # 1초
    balance = await account_client.get_balance(payment_id)  # 1초
    fee = await fee_client.calculate(payment_id)         # 1초
    return {&quot;fraud&quot;: fraud, &quot;balance&quot;: balance, &quot;fee&quot;: fee}

# === 동시 실행 (좋은 예) ===
async def enrich_payment_concurrent(payment_id: UUID) -&amp;gt; dict:
    &quot;&quot;&quot;3개 API를 동시에 호출 &amp;rarr; 총 1초 (가장 느린 것 기준)&quot;&quot;&quot;
    fraud, balance, fee = await asyncio.gather(
        fraud_client.check(payment_id),
        account_client.get_balance(payment_id),
        fee_client.calculate(payment_id),
    )
    return {&quot;fraud&quot;: fraud, &quot;balance&quot;: balance, &quot;fee&quot;: fee}

# === 동시 실행 + 개별 에러 처리 ===
async def enrich_payment_safe(payment_id: UUID) -&amp;gt; dict:
    &quot;&quot;&quot;일부 실패해도 나머지는 사용&quot;&quot;&quot;
    results = await asyncio.gather(
        fraud_client.check(payment_id),
        account_client.get_balance(payment_id),
        fee_client.calculate(payment_id),
        return_exceptions=True,  # 예외를 반환값으로 전환
    )

    return {
        &quot;fraud&quot;: results[0] if not isinstance(results[0], Exception) else None,
        &quot;balance&quot;: results[1] if not isinstance(results[1], Exception) else None,
        &quot;fee&quot;: results[2] if not isinstance(results[2], Exception) else None,
        &quot;errors&quot;: [
            str(r) for r in results if isinstance(r, Exception)
        ],
    }

# === TaskGroup ===
async def enrich_payment_taskgroup(payment_id: UUID) -&amp;gt; dict:
    &quot;&quot;&quot;하나라도 실패하면 나머지도 취소&quot;&quot;&quot;
    async with asyncio.TaskGroup() as tg:
        fraud_task = tg.create_task(fraud_client.check(payment_id))
        balance_task = tg.create_task(account_client.get_balance(payment_id))
        fee_task = tg.create_task(fee_client.calculate(payment_id))

    # TaskGroup 블록을 벗어나면 모든 Task 완료 보장
    return {
        &quot;fraud&quot;: fraud_task.result(),
        &quot;balance&quot;: balance_task.result(),
        &quot;fee&quot;: fee_task.result(),
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 많아지면 Rate Limit를 강제해야하는 경우가 무조건 생긴다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 다음과 같이 처리 가능하다. (Semaphore를 통한 동시성 제한)&lt;/p&gt;
&lt;pre id=&quot;code_1774872171149&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;semaphore = asyncio.Semaphore(10)  # 동시 10개까지

async def call_with_limit(url: str) -&amp;gt; dict:
    async with semaphore:
        async with httpx.AsyncClient() as client:
            response = await client.get(url)
            return response.json()

# 100개 요청을 동시에 10개씩 처리
results = await asyncio.gather(
    *[call_with_limit(f&quot;/api/item/{i}&quot;) for i in range(100)]
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;async def vs def&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제 async를 사용해야할까? 어떤 점에 유의해야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async를 사용하면 async로 선언하면 된다. 너무 당연하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 async 내에서는 딱히 유의할게 없을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음을 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1774872373844&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@router.get(&quot;/report&quot;)
async def generate_report():
    # time.sleep은 블로킹! 이벤트 루프 전체를 3초간 멈춤
    time.sleep(3)  # 절대 금지

    # requests.get은 블로킹! httpx.AsyncClient를 써야 함
    result = requests.get(&quot;https://api.example.com&quot;)  # 절대 금지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말한것처럼 FastAPI는 하나의 Event Loop를 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 블로킹 함수를 async 안에서 선언하면 매우 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 어떻게 처리할 수 있을까? 방법은 다양할 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;블로킹 함수를 async로 처리한다.&lt;/li&gt;
&lt;li&gt;함수를 애당초 동기 함수로 만들어버린다.&lt;/li&gt;
&lt;li&gt;블로킹 함수를 별도 쓰레드로 넘긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# async 함수 + async 라이브러리
@router.get(&quot;/report&quot;)
async def generate_report():
    await asyncio.sleep(3)  # 비동기 sleep
    async with httpx.AsyncClient() as client:
        result = await client.get(&quot;https://api.example.com&quot;)  # 비동기 HTTP&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774872618128&quot; class=&quot;applescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 동기 함수 (블로킹 코드를 써야 할 때)
@router.get(&quot;/report&quot;)
def generate_report():  # async 아님!
    # FastAPI가 자동으로 스레드풀에서 실행
    time.sleep(3)  # 이건 괜찮음 (별도 스레드)
    result = requests.get(&quot;https://api.example.com&quot;)  # 이것도 괜찮음&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774872605699&quot; class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# async 함수 안에서 동기 코드를 실행해야 할 때
@router.get(&quot;/report&quot;)
async def generate_report():
    # 동기 함수를 스레드풀에 위임
    result = await asyncio.to_thread(
        generate_heavy_pdf,  # CPU 바운드 동기 함수
        data=report_data,
    )
    return FileResponse(result)​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 헷갈릴 수 있어서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 마주할 수 있는 상황을 기준으로 다음과 같이 정리해본다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;async 라이브러리 사용 (asyncpg, httpx, aioredis)&lt;/td&gt;
&lt;td&gt;async def&lt;/td&gt;
&lt;td&gt;논블로킹 I/O 활용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동기 라이브러리 사용 (psycopg2, requests)&lt;/td&gt;
&lt;td&gt;def&lt;/td&gt;
&lt;td&gt;FastAPI가 스레드풀 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU 연산 (PDF 생성, 암호화, 데이터 변환)&lt;/td&gt;
&lt;td&gt;def&lt;/td&gt;
&lt;td&gt;CPU 바운드는 async 의미 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 외부 서비스 동시 호출&lt;/td&gt;
&lt;td&gt;async def&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;gather&lt;/td&gt;
&lt;td&gt;병렬 I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 읽기/쓰기&lt;/td&gt;
&lt;td&gt;def&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;aiofiles&lt;/td&gt;
&lt;td&gt;디스크 I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket 처리&lt;/td&gt;
&lt;td&gt;async def&lt;/td&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의심스러울 때&lt;/td&gt;
&lt;td&gt;def&lt;/td&gt;
&lt;td&gt;안전한 기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드/FastAPI</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/25</guid>
      <comments>https://leestana01.tistory.com/25#entry25comment</comments>
      <pubDate>Mon, 30 Mar 2026 20:41:18 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 기본부터 극한까지 날먹하기 (SpringBoot에서 넘어가기)</title>
      <link>https://leestana01.tistory.com/24</link>
      <description>&lt;h1&gt;용어부터 알아보자&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot와 FastAPI는 어떻게 다를까? 기본적인 구조부터 잡고 가면 편할 것 같다.&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;JDK (Java 17/21)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;Python (3.11/3.12/3.13)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Maven / Gradle&lt;/td&gt;
&lt;td&gt;Poetry / uv / pip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;application.yml&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;.env + pydantic-settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Tomcat (내장 서블릿 컨테이너)&lt;/td&gt;
&lt;td&gt;Uvicorn (ASGI 서버)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Spring Initializr&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;수동 구성 (or cookiecutter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;./gradlew bootRun&lt;/td&gt;
&lt;td&gt;uvicorn app.main:app --reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;JAR 패키징&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;Docker 이미지 (사실상 표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;JUnit + Mockito&lt;/td&gt;
&lt;td&gt;pytest + pytest-asyncio + httpx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Checkstyle + SpotBugs&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;Ruff + mypy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Lombok&lt;/td&gt;
&lt;td&gt;dataclass / Pydantic (언어 자체에 내장)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보기만 해도 바이트코드로 컴파일해서 동작하는 Java의 위대함이 다시 돋보인다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼에도 이 열악한 환경에서 간편하고 개발친화적인 환경을 구축한 FastAPI에 경이로움을 표한다.&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;h1&gt;바꿔야 할 사고방식&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot에 익숙하다면 FastAPI로 넘어가는 것은 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나&amp;nbsp;그 속도를 빠르게 하려면, 우선 Python으로 사고하는 법을 익힐 필요가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모든 것이 클래스가 아니다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java: 모든 것이 클래스 안에 존재&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java는 유틸리티 함수 하나를 위해서도 클래스가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;public class FeeCalculator {
    public static int calculate(int amount) {
        return Math.max(amount * 1 / 1000, 500);
    }
}

// 호출: FeeCalculator.calculate(100000)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python: 함수가 독립적으로 존재&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬은 파일 자체가 모듈이다. 즉, 클래스가 불필요하다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;def calculate_fee(amount: int) -&amp;gt; int:
    return max(amount // 1000, 500)

# 호출: from app.domain.payment.fee_utils import calculate_fee&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;Class는 언제 만들어야할까?&lt;/b&gt; 쉽게 정리해봤다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Class가 필요한 겨우&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;함수로 충분한 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상태(state)를 가질 때&lt;br /&gt;&lt;/b&gt;초기화 시 설정을 받고, 메서드마다 그 설정을 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 메서드가 공유하는 컨텍스트가 있을 때&lt;br /&gt;&lt;/b&gt;DB 세션, HTTP 클라이언트 등...&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다형성이 필요할 때&lt;br /&gt;&lt;/b&gt;Protocol/ABC로 인터페이스 정의, 여러 구현체&lt;/li&gt;
&lt;/ol&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력 &amp;rarr; 출력이 명확한 변환/계산 로직&lt;/li&gt;
&lt;li&gt;유틸리티 함수&lt;/li&gt;
&lt;li&gt;간단한 팩토리&lt;/li&gt;
&lt;/ol&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Interface가 없다 &amp;rarr; Protocol을 쓴다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java: Interface + 구현체&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface PaymentGateway {
    PaymentResult charge(ChargeRequest request);
    PaymentResult refund(RefundRequest request);
}

@Service
public class TossPaymentGateway implements PaymentGateway {
    @Override
    public PaymentResult charge(ChargeRequest request) { ... }
    @Override
    public PaymentResult refund(RefundRequest request) { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python: Protocol (구조적 서브타이핑)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol은 Java의 Interface에 해당한다. 그러나 근본적인 차이가 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Interface : 명시적으로 implements를 선언해야 함&lt;/li&gt;
&lt;li&gt;Protocol : 메서드 시그니처만 맞으면 자동 호환 (덕 타이핑)&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕 타이핑이란? Gemini의 예시를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1774867864063&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Duck:
    def quack(self):
        print(&quot;꽥꽥!&quot;)

class Person:
    def quack(self):
        print(&quot;사람이 오리 소리를 냅니다.&quot;)

def make_it_quack(animal):
    # animal이 무엇이든 상관없이 quack() 메서드만 있으면 작동
    animal.quack()

duck = Duck()
person = Person()

make_it_quack(duck)   # 꽥꽥!
make_it_quack(person) # 사람이 오리 소리를 냅니다.&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;실제&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;방식이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from typing import Protocol, runtime_checkable

@runtime_checkable
class PaymentGateway(Protocol):
    async def charge(self, request: ChargeRequest) -&amp;gt; PaymentResult: ...
    async def refund(self, request: RefundRequest) -&amp;gt; PaymentResult: ...

# implements 키워드가 없다!
class TossPaymentGateway:
    &quot;&quot;&quot;Protocol에 정의된 메서드를 구현하면 자동으로 PaymentGateway 호환&quot;&quot;&quot;

    def __init__(self, api_key: str, base_url: str):
        self.client = httpx.AsyncClient(
            base_url=base_url,
            headers={&quot;Authorization&quot;: f&quot;Basic {api_key}&quot;},
        )

    async def charge(self, request: ChargeRequest) -&amp;gt; PaymentResult:
        response = await self.client.post(&quot;/v1/payments&quot;, json=request.model_dump())
        return PaymentResult(**response.json())

    async def refund(self, request: RefundRequest) -&amp;gt; PaymentResult:
        response = await self.client.post(
            f&quot;/v1/payments/{request.payment_key}/cancel&quot;,
            json={&quot;cancelReason&quot;: request.reason},
        )
        return PaymentResult(**response.json())

# 다른 PG사 구현체
class KakaoPayGateway:
    async def charge(self, request: ChargeRequest) -&amp;gt; PaymentResult: ...
    async def refund(self, request: RefundRequest) -&amp;gt; PaymentResult: ...

# 타입 체커가 PaymentGateway 호환 여부를 검증
def get_gateway(gateway_type: str) -&amp;gt; PaymentGateway:
    if gateway_type == &quot;toss&quot;:
        return TossPaymentGateway(api_key=&quot;...&quot;, base_url=&quot;...&quot;)
    elif gateway_type == &quot;kakao&quot;:
        return KakaoPayGateway(...)
    raise ValueError(f&quot;Unknown gateway: {gateway_type}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ABC(Abstract Base Class) - 명시적 상속이 필요할 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Protocol과 달리 명시적 상속을 강제한다. Java의 abstract class에 가장 가깝다.&lt;br /&gt;언제 ABC를 쓰고 언제 Protocol을 써야할 까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ABC: 공통 구현 로직이 있을 때 (Template Method 패턴)&lt;/li&gt;
&lt;li&gt;Protocol: 인터페이스 계약만 정의할 때 (Strategy 패턴)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    def __init__(self, api_key: str, base_url: str):
        # 공통 초기화 (= Java abstract class의 constructor)
        self.client = httpx.AsyncClient(
            base_url=base_url,
            headers=self._build_headers(api_key),
            timeout=30.0,
        )

    @abstractmethod
    def _build_headers(self, api_key: str) -&amp;gt; dict[str, str]:
        &quot;&quot;&quot;각 PG사별 인증 헤더 포맷이 다름&quot;&quot;&quot;
        ...

    @abstractmethod
    async def charge(self, request: ChargeRequest) -&amp;gt; PaymentResult: ...

    @abstractmethod
    async def refund(self, request: RefundRequest) -&amp;gt; PaymentResult: ...

    # 공통 메서드 (= Java abstract class의 concrete method)
    async def health_check(self) -&amp;gt; bool:
        try:
            response = await self.client.get(&quot;/health&quot;)
            return response.status_code == 200
        except httpx.HTTPError:
            return False

class TossPaymentGateway(PaymentGateway):  # 명시적 상속
    def _build_headers(self, api_key: str) -&amp;gt; dict[str, str]:
        return {&quot;Authorization&quot;: f&quot;Basic {api_key}&quot;}

    async def charge(self, request: ChargeRequest) -&amp;gt; PaymentResult: ...
    async def refund(self, request: RefundRequest) -&amp;gt; PaymentResult: ...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타입 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java는 &lt;b&gt;컴파일 타임에 타입이 강제&lt;/b&gt;된다. (즉, 타입 오류 = 컴파일 실패)&lt;br /&gt;Python은 &lt;b&gt;런타임 언어&lt;/b&gt;이다. 타입 힌트는 &quot;선택적 문서&quot;이며, 런타임에 무시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 FastAPI/Pydantic이 타입 힌트를 &lt;b&gt;런타임에 활용&lt;/b&gt;하면서 상황이 바뀐다&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from typing import Annotated, TypeVar, Generic
from pydantic import BaseModel, Field

# === 기본 타입 힌트 ===
def add(a: int, b: int) -&amp;gt; int:      # Java의 int add(int a, int b)
    return a + b

# === Optional 처리 ===
# Java: Optional&amp;lt;String&amp;gt;
# Python:
name: str | None = None               # Python 3.10+ 문법
name: Optional[str] = None            # 구버전 문법 (동일 의미)

# === 제네릭 ===
# Java: public class ApiResponse&amp;lt;T&amp;gt; { T data; String message; }
T = TypeVar(&quot;T&quot;)

class ApiResponse(BaseModel, Generic[T]):
    data: T
    message: str
    success: bool = True

# 사용:
class PaymentListResponse(BaseModel):
    payments: list[PaymentResponse]
    total_count: int

response: ApiResponse[PaymentListResponse]  # 구체 타입 지정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에는 없는 것도 있다. FastAPI의 꽤나 강력한 무기라고 생각한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774868401146&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# === Annotated ===
# Java에는 없는 개념. 타입에 메타데이터를 부착한다.
from fastapi import Depends, Query

# Before (보일러플레이트)
@router.get(&quot;/payments&quot;)
async def list_payments(
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    user: CurrentUser = Depends(get_current_user),
): ...

# After (Annotated로 재사용 가능한 타입 정의)
Page = Annotated[int, Query(ge=1, description=&quot;페이지 번호&quot;)]
PageSize = Annotated[int, Query(ge=1, le=100, description=&quot;페이지 크기&quot;)]
DBSession = Annotated[AsyncSession, Depends(get_db)]
AuthUser = Annotated[CurrentUser, Depends(get_current_user)]

@router.get(&quot;/payments&quot;)
async def list_payments(
    page: Page = 1,
    size: PageSize = 20,
    db: DBSession,
    user: AuthUser,
): ...
# &amp;rarr; 타입 + DI + 검증이 하나의 타입 별칭으로 통합&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mypy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 컴파일러의 타입 검사에 해당한다. 필히 다음에 꼭 유의하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;mypy strict 모드&lt;/b&gt;를 처음부터 적용할 것. 나중에 켜면 수천 개의 에러가 쏟아진다.&lt;/li&gt;
&lt;li&gt;Pydantic, SQLAlchemy 용 mypy 플러그인을 반드시 설정할 것.&lt;/li&gt;
&lt;li&gt;CI/CD에 mypy 검사를 포함할 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;shell&quot;&gt;&lt;code&gt;# 타입 체크 실행 (= javac의 타입 검사 부분만 수행)
mypy app/

# 에러 예시:
# app/service.py:42: error: Argument 1 to &quot;withdraw&quot; has incompatible type &quot;str&quot;; expected &quot;int&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데코레이터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP(@Transactional, @Cacheable, @Async, @PreAuthorize)의 역할(이걸 &lt;b&gt;횡단 관심사&lt;/b&gt; 라고 하더라..)에 대해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서는 &lt;b&gt;데코레이터&lt;/b&gt;가 그 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import functools
import time
import structlog
from typing import Callable, ParamSpec, TypeVar

P = ParamSpec(&quot;P&quot;)
R = TypeVar(&quot;R&quot;)
logger = structlog.get_logger()

# === 성능 측정 데코레이터 (= Spring AOP의 @Around 어드바이스) ===
def measure_time(func: Callable[P, R]) -&amp;gt; Callable[P, R]:
    @functools.wraps(func)
    async def wrapper(*args: P.args, **kwargs: P.kwargs) -&amp;gt; R:
        start = time.perf_counter()
        try:
            result = await func(*args, **kwargs)
            return result
        finally:
            duration = (time.perf_counter() - start) * 1000
            logger.info(
                &quot;function_executed&quot;,
                function=func.__name__,
                duration_ms=round(duration, 2),
            )
    return wrapper

# === 재시도 데코레이터 (= Spring Retry의 @Retryable) ===
def retry(
    max_attempts: int = 3,
    delay: float = 1.0,
    backoff_factor: float = 2.0,
    exceptions: tuple[type[Exception], ...] = (Exception,),
):
    def decorator(func: Callable[P, R]) -&amp;gt; Callable[P, R]:
        @functools.wraps(func)
        async def wrapper(*args: P.args, **kwargs: P.kwargs) -&amp;gt; R:
            last_exception: Exception | None = None
            current_delay = delay

            for attempt in range(1, max_attempts + 1):
                try:
                    return await func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt &amp;lt; max_attempts:
                        logger.warn(
                            &quot;retry_attempt&quot;,
                            function=func.__name__,
                            attempt=attempt,
                            max_attempts=max_attempts,
                            delay=current_delay,
                            error=str(e),
                        )
                        await asyncio.sleep(current_delay)
                        current_delay *= backoff_factor

            raise last_exception  # type: ignore
        return wrapper
    return decorator

# === 사용 ===
class PGGatewayClient:

    @measure_time
    @retry(max_attempts=3, delay=0.5, exceptions=(httpx.HTTPError, TimeoutError))
    async def charge(self, request: ChargeRequest) -&amp;gt; PaymentResult:
        response = await self.client.post(&quot;/v1/payments&quot;, json=request.model_dump())
        response.raise_for_status()
        return PaymentResult(**response.json())&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;style2&quot;&gt;알면 좋은 Spring AOP와의 차이&lt;br /&gt;- Spring AOP는 프록시 기반이므로, 같은 클래스 내에서 this.method()를 호출하면 AOP가 동작하지 않는 유명한 함정이 있다. Python 데코레이터는 함수 자체를 래핑하므로 이 문제가 없다.&lt;br /&gt;- Spring AOP는 런타임에 프록시 객체를 생성하므로 디버깅이 어렵다. Python 데코레이터는 스택 트레이스가 명확하다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨텍스트 매니저&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 SpringBoot에서 넘어가면 좀 번거롭게 느껴지는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드로 설명을 대체한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# Java의 try-with-resources:
# try (Connection conn = dataSource.getConnection()) { ... }

# Python의 context manager:
async with async_session_factory() as session:
    # session 사용
    ...
# 자동으로 close/rollback

# 직접 만들기 (= AutoCloseable 구현)
from contextlib import asynccontextmanager

@asynccontextmanager
async def transaction(db: AsyncSession):
    &quot;&quot;&quot;
    명시적 트랜잭션 관리.
    Spring의 @Transactional(propagation = REQUIRES_NEW)와 유사한 역할.
    &quot;&quot;&quot;
    try:
        yield db
        await db.commit()
    except Exception:
        await db.rollback()
        raise
    finally:
        await db.close()

# 사용
async def transfer_money(sender_id: UUID, receiver_id: UUID, amount: int):
    async with transaction(db) as session:
        await debit(session, sender_id, amount)
        await credit(session, receiver_id, amount)
        # 블록을 벗어나면 자동 commit, 예외 시 자동 rollback&lt;/code&gt;&lt;/pre&gt;</description>
      <category>백엔드/FastAPI</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/24</guid>
      <comments>https://leestana01.tistory.com/24#entry24comment</comments>
      <pubDate>Mon, 30 Mar 2026 20:14:30 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 기본부터 극한까지 날먹하기</title>
      <link>https://leestana01.tistory.com/23</link>
      <description>&lt;h1&gt;서론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 FastAPI 강의는 지양한다. 그럴거면 ChatGPT에게 물어보는게 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 구조부터 모든 한 줄마다 연구하고 최적의 방향을 서술할 것이다.&lt;/p&gt;
&lt;h1&gt;환경 구축&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Python 버전 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 설치 방법까지는 언급하지 않겠다. 그러나 경우에 따라 Python의 버전 관리가 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 sdkman으로 JDK 17, 21을 전환하듯, Python에서는 pyenv를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 설치
curl https://pyenv.run | bash

# ~/.bashrc 또는 ~/.zshrc에 추가
export PYENV_ROOT=&quot;$HOME/.pyenv&quot;
export PATH=&quot;$PYENV_ROOT/bin:$PATH&quot;
eval &quot;$(pyenv init -)&quot;

# Python 버전 설치 및 전환
pyenv install 3.12.4
pyenv install 3.11.9
pyenv global 3.12.4          # 시스템 전체 기본
pyenv local 3.11.9           # 현재 디렉토리에서만 (.python-version 파일 생성)

# 설치된 버전 확인
pyenv versions&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 초기화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 천국 Python 답게 패키지매니저도 다양하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;venv + pip (비추)&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 requirements.txt에 의존한다는 것.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;lock 파일이 아니므로 재현성 보장 안 된다는 의견도 있으나, 보통 pip freeze 쓰니깐 상관 없을 듯&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발/프로덕션 의존성 분리 불가&lt;/li&gt;
&lt;li&gt;의존성 해결이 단순해 충돌 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# 디렉토리 생성
mkdir practice-api &amp;amp;&amp;amp; cd practice-api

# 가상환경 생성 (= 이 프로젝트 전용 Python 사본 생성)
python3 -m venv .venv

# 활성화
source .venv/bin/activate          # Linux/Mac
# .venv\Scripts\activate           # Windows

# 프롬프트가 바뀜:
# (.venv) user@host:~/practice-api$

# pip 업그레이드 후 패키지 설치
pip install --upgrade pip
pip install fastapi &quot;uvicorn[standard]&quot; sqlalchemy alembic pydantic-settings

# 설치 기록 (= 의존성 스냅샷)
pip freeze &amp;gt; requirements.txt

# 다른 환경에서 복원
pip install -r requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;poetry&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;gradle과 유사한 느낌이 든다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;UV와 성능차이가 궁금해서 조사하다가 아래 블로그를 발견했다. 궁금하면 들어가보시길&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://devocean.sk.com/blog/techBoardDetail.do?ID=167425&amp;amp;boardType=techBlog&quot;&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=167425&amp;amp;boardType=techBlog&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775998127818&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;Python Poetry 대신 UV를 써보면서 느낀 점들&quot; data-og-description=&quot; &quot; data-og-host=&quot;devocean.sk.com&quot; data-og-source-url=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=167425&amp;amp;boardType=techBlog&quot; data-og-url=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=167425&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cvshJ7/dJMb9iaSlWn/ehD7CAcQe2QKX2VN7fApbk/img.png?width=360&amp;amp;height=202&amp;amp;face=0_0_360_202,https://scrap.kakaocdn.net/dn/sbIPs/dJMb9fZwxsY/IBkBnRw2SUE4JCaERFPKJ0/img.png?width=360&amp;amp;height=202&amp;amp;face=0_0_360_202,https://scrap.kakaocdn.net/dn/8hoo7/dJMb9jgytK5/RyPdukEHHzy7yfdOXumCE0/img.jpg?width=720&amp;amp;height=714&amp;amp;face=0_0_720_714&quot;&gt;&lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=167425&amp;amp;boardType=techBlog&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=167425&amp;amp;boardType=techBlog&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cvshJ7/dJMb9iaSlWn/ehD7CAcQe2QKX2VN7fApbk/img.png?width=360&amp;amp;height=202&amp;amp;face=0_0_360_202,https://scrap.kakaocdn.net/dn/sbIPs/dJMb9fZwxsY/IBkBnRw2SUE4JCaERFPKJ0/img.png?width=360&amp;amp;height=202&amp;amp;face=0_0_360_202,https://scrap.kakaocdn.net/dn/8hoo7/dJMb9jgytK5/RyPdukEHHzy7yfdOXumCE0/img.jpg?width=720&amp;amp;height=714&amp;amp;face=0_0_720_714');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Python Poetry 대신 UV를 써보면서 느낀 점들&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devocean.sk.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# Poetry 설치 (공식 방법)
curl -sSL https://install.python-poetry.org | python3 -

# 프로젝트 초기화
mkdir practice-api &amp;amp;&amp;amp; cd practice-api
poetry init --no-interaction

# 핵심 의존성 추가 (= implementation 'group:artifact:version')
poetry add fastapi
poetry add &quot;uvicorn[standard]&quot;
poetry add sqlalchemy[asyncio]
poetry add asyncpg                # PostgreSQL async 드라이버
poetry add alembic
poetry add pydantic-settings
poetry add structlog              # 구조화 로깅
poetry add httpx                  # async HTTP 클라이언트
poetry add &quot;redis[hiredis]&quot;       # Redis 클라이언트 (성능 최적화)
poetry add &quot;python-jose[cryptography]&quot;  # JWT

# 개발 의존성 (= testImplementation, developmentOnly)
poetry add --group dev pytest
poetry add --group dev pytest-asyncio
poetry add --group dev pytest-cov
poetry add --group dev httpx         # 테스트용 async HTTP 클라이언트
poetry add --group dev ruff          # 린터 + 포매터 (통합)
poetry add --group dev mypy          # 타입 체커
poetry add --group dev factory-boy   # 테스트 픽스처 팩토리
poetry add --group dev faker         # 테스트 데이터 생성

# 의존성 설치 (= gradle build)
poetry install

# 실행
poetry run uvicorn app.main:app --reload&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pyproject.toml&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;build.gradle 과 유사하다. Node 사용자라면 package-lock 생각해도 될 듯.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[tool.poetry]
name = &quot;practice-api&quot;
version = &quot;0.1.0&quot;
description = &quot;API 연습용 서버&quot;
authors = [&quot;Team Lead &amp;lt;lead@practice.com&amp;gt;&quot;]
readme = &quot;README.md&quot;

[tool.poetry.dependencies]
python = &quot;^3.12&quot;
fastapi = &quot;^0.115.0&quot;
uvicorn = {extras = [&quot;standard&quot;], version = &quot;^0.30.0&quot;}
sqlalchemy = {extras = [&quot;asyncio&quot;], version = &quot;^2.0&quot;}
asyncpg = &quot;^0.30.0&quot;
alembic = &quot;^1.14&quot;
pydantic-settings = &quot;^2.5&quot;
structlog = &quot;^24.4&quot;
httpx = &quot;^0.27&quot;
redis = {extras = [&quot;hiredis&quot;], version = &quot;^5.0&quot;}
python-jose = {extras = [&quot;cryptography&quot;], version = &quot;^3.3&quot;}

[tool.poetry.group.dev.dependencies]
pytest = &quot;^8.3&quot;
pytest-asyncio = &quot;^0.24&quot;
pytest-cov = &quot;^5.0&quot;
ruff = &quot;^0.6&quot;
mypy = &quot;^1.11&quot;
factory-boy = &quot;^3.3&quot;
faker = &quot;^30.0&quot;

# === 린터/포매터 설정 (= Checkstyle 설정) ===
[tool.ruff]
target-version = &quot;py312&quot;
line-length = 100

[tool.ruff.lint]
select = [
    &quot;E&quot;,    # pycodestyle errors
    &quot;W&quot;,    # pycodestyle warnings
    &quot;F&quot;,    # pyflakes
    &quot;I&quot;,    # isort (import 정렬)
    &quot;N&quot;,    # pep8-naming
    &quot;UP&quot;,   # pyupgrade
    &quot;B&quot;,    # flake8-bugbear
    &quot;SIM&quot;,  # flake8-simplify
    &quot;ASYNC&quot;,# flake8-async
]

# === 타입 체커 설정 (= Java 컴파일러의 타입 검사에 해당) ===
[tool.mypy]
python_version = &quot;3.12&quot;
strict = true                    # 가장 엄격한 모드
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true     # 타입 힌트 없는 함수 금지

# === 테스트 설정 ===
[tool.pytest.ini_options]
asyncio_mode = &quot;auto&quot;            # async 테스트 자동 감지
testpaths = [&quot;tests&quot;]
addopts = &quot;-v --tb=short&quot;

[build-system]
requires = [&quot;poetry-core&quot;]
build-backend = &quot;poetry.core.masonry.api&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;uv (매우 추천 &amp;amp; 최신 표준)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-size: 1.44em; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;uv (차세대 표준)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uv는 Rust로 작성된 초고속 패키지 매니저이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pip 대비 10~100배 빠르다고 알려져있어서 Poetry 대안으로 요즘 뜨고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세상 깔끔하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;주의!&lt;br /&gt;현재 app 폴더를 만들지 않았으므로, 실행 시 발생하는 에러는 정상입니다.&lt;br /&gt;&lt;span style=&quot;background-color: #666666; color: #ffffff;&quot;&gt;&amp;nbsp; uv run uvicorn app.main:app --reload&amp;nbsp; &lt;/span&gt;&amp;nbsp; &amp;nbsp;&amp;rarr;&amp;nbsp; &amp;nbsp;에러&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# 설치
curl -LsSf https://astral.sh/uv/install.sh | sh

# 프로젝트 초기화
uv init practice-api
cd practice-api

# 의존성 추가 (lock 파일 자동 생성)
uv add fastapi &quot;uvicorn[standard]&quot; sqlalchemy asyncpg
uv add --dev pytest ruff mypy

# 실행 (가상환경 자동 생성/관리)
uv run uvicorn app.main:app --reload

# 동기화 (= poetry install)
uv sync&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 환경 표준화 (생략해도 좋음)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 통상적인 컨벤션이다. 필요 시 더보기를 통해 확인하면 된다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;.editorconfig (= 팀 코딩 스타일 통일)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yml,yaml,json}]
indent_size = 2&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;pre-commit (= Git 컨벤션)&lt;/h3&gt;
&lt;pre class=&quot;vim&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0
    hooks:
      - id: ruff          # 린트
        args: [--fix]
      - id: ruff-format   # 포맷

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.11.0
    hooks:
      - id: mypy
        additional_dependencies: [pydantic, sqlalchemy[mypy]]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;pip install pre-commit
pre-commit install
# 이후 모든 git commit 시 자동으로 린트/포맷/타입체크 실행&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;VS Code 설정&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;json&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// .vscode/settings.json
{
    &quot;python.defaultInterpreterPath&quot;: &quot;.venv/bin/python&quot;,
    &quot;python.analysis.typeCheckingMode&quot;: &quot;strict&quot;,
    &quot;[python]&quot;: {
        &quot;editor.defaultFormatter&quot;: &quot;charliermarsh.ruff&quot;,
        &quot;editor.formatOnSave&quot;: true,
        &quot;editor.codeActionsOnSave&quot;: {
            &quot;source.fixAll.ruff&quot;: &quot;explicit&quot;,
            &quot;source.organizeImports.ruff&quot;: &quot;explicit&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hello World&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 진짜 시작해보자. 먼저 다음 두 파일을 생성하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app/__init__.py : 빈 파일로 생성한다. (없으면 에러. Python 패키지 선언)&lt;/li&gt;
&lt;li&gt;app/main.py&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# app/main.py
from fastapi import FastAPI

app = FastAPI(
    title=&quot;FastAPI 연습 API&quot;,
    version=&quot;0.1.0&quot;,
    description=&quot;FastAPI 연습용 API 스펙&quot;,
)

@app.get(&quot;/&quot;)
async def root():
    return {&quot;message&quot;: &quot;Hello, World&quot;}

@app.get(&quot;/health&quot;)
async def health_check():
    return {&quot;status&quot;: &quot;healthy&quot;, &quot;service&quot;: &quot;practive-api&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 실행
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app.main&amp;nbsp; &amp;nbsp;&amp;rarr; app/ 내의 main.py 파일&lt;br /&gt;:app&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; 그 안의 app 변수 (FastAPI 인스턴스)&lt;br /&gt;--reload&amp;nbsp; &amp;nbsp; &amp;rarr; 파일 변경 감지 자동 재시작&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 확인해보자. FastAPI는 Swagger가 내장이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API: &lt;code&gt;http://localhost:8000/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Swagger UI: &lt;code&gt;http://localhost:8000/docs&lt;/code&gt; &amp;larr; &lt;b&gt;킬러 기능. 별도 라이브러리 불필요&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ReDoc: &lt;code&gt;http://localhost:8000/redoc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OpenAPI JSON: &lt;code&gt;http://localhost:8000/openapi.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;FastAPI 핵심 개념&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깔끔하게 다음에 대해 설명한다. 이 정도만 알아도 FastAPI의 코드를 이해하는데 문제가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글로 3분만에 개념을 날먹해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API(라우팅) 작성 방법&lt;/li&gt;
&lt;li&gt;Pydantic&lt;/li&gt;
&lt;li&gt;Depends&lt;/li&gt;
&lt;li&gt;Annotated&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라우팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 CRUD 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전혀 어려울게 없다.&amp;nbsp;기본적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1774869499047&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@router.{post/get/put/patch/delete}(
	스웨거 명세
)
async def 함수명 (인자) -&amp;gt; 결과타입:
	함수 코드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자용 상세설명은 더보기 클릭.&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 다음을 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1776060654503&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# GET &amp;mdash; 단건 조회 (Path 파라미터)
@router.get(
    &quot;/{payment_id}&quot;, 
    response_model=PaymentResponse,
    status_code=status.HTTP_200_OK,
    responses={
        404: {&quot;description&quot;: &quot;없는 결제 내역&quot;},
        422: {&quot;description&quot;: &quot;요청 데이터 검증 실패&quot;},
    },
)
async def get_payment(
    payment_id: UUID = Path(..., description=&quot;결제 고유 ID&quot;),
    db: DBSession,
    user: AuthUser,
) -&amp;gt; PaymentResponse:
    ...(구현할 코드)...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@router.get() 분석&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 인자는 api경로이다.&amp;nbsp;&lt;b&gt;/{payment_id}&lt;/b&gt;&amp;nbsp; &lt;br /&gt;&amp;rarr; 즉, &lt;b&gt;https://서버주소/{payment_id}&lt;/b&gt;로 GET 요청하면 접근 가능하다는 것이다.&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;rarr;&amp;nbsp;&lt;/span&gt;결제 ID가 &lt;b&gt;1234-5678-9012-3456&lt;/b&gt;이라면? &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;b&gt;https://서버주소/&lt;/b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;b&gt;1234-5678-9012-3456&lt;/b&gt; 로 접근하면 된다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;두 번째 인자부터는 의미 없다. 그냥 Swagger 전용 Spec 문서이다.&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;rarr; 기본 응답값 200, 그 외에는 상황에 따른 응답값&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;async def get_payment() -&amp;gt; PaymentResponse 분석&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;인자는 payment_id, db, user를 받는다.&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;rarr;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;payment_id 는 사용자로부터 받는다.&lt;br /&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;rarr;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;DBSession과 AuthUser는 사용자에게 받지 않고, FastAPI가 자동으로 주입(DI)한다.&lt;br /&gt;&lt;br /&gt;DBSession과 AuthUser가 자동 주입되는 것은 현재 이해되지 않는 것이 당연하다. &lt;br /&gt;Depends와 Annotated의 조합형태이다. 본문 하단에 자세히 후술하겠다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;결과 형태는 PaymentResponse이다. &lt;br /&gt;&lt;b&gt;async def get_payment(인자) -&amp;gt; PaymentResponse&lt;/b&gt; 형태로 결과 형태를 정의했다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전 코드를 보면 이해가 쉬울 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더보기 없이 아래 코드만 읽어봐도 사용법이 바로 감 잡힐 것이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;from fastapi import APIRouter, Path, Query, Body, Header, status
from uuid import UUID

router = APIRouter(
    prefix=&quot;/api/v1/payments&quot;,
    tags=[&quot;Payments&quot;],          # Swagger UI에서 그룹핑
    responses={
        401: {&quot;description&quot;: &quot;인증 실패&quot;},
        403: {&quot;description&quot;: &quot;권한 없음&quot;},
    },
)

# GET &amp;mdash; 단건 조회 (Path 파라미터)
@router.get(&quot;/{payment_id}&quot;, response_model=PaymentResponse)
async def get_payment(
    payment_id: UUID = Path(..., description=&quot;결제 고유 ID&quot;),
    db: DBSession,
    user: AuthUser,
) -&amp;gt; PaymentResponse:
    ...

# POST &amp;mdash; 생성
@router.post(
    &quot;/&quot;,
    status_code=status.HTTP_201_CREATED,
    response_model=PaymentResponse,
    responses={
        409: {&quot;description&quot;: &quot;중복 결제 요청&quot;},
        422: {&quot;description&quot;: &quot;요청 데이터 검증 실패&quot;},
    },
)
async def create_payment(
    request: CreatePaymentRequest = Body(...),
    idempotency_key: str = Header(..., alias=&quot;Idempotency-Key&quot;),
    db: DBSession,
    user: AuthUser,
) -&amp;gt; PaymentResponse:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와! 당신은 벌써 api를 찍어낼 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 데이터 저장하는 방법만 안다면 당신도 지금 해커톤 가능!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;(심화 내용 &amp;rarr; 지금 이해 안가면 무시하고 넘어가시면 됩니다.)&lt;br /&gt;잠깐! DB 연결을 router(api)에서 인자로 받는다고요? &lt;br /&gt;이러면 api 구현할 때마다 인자에 번거롭게 DBSession을 써야하는데, &lt;br /&gt;실제 로직을 구현할 파일(Service)에서 주입하면 인자도 줄고 좋지 않나요?&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충분히 의문이 들 수 있다. 그러나 이는 FastAPI에서 명시성을 위해 의도한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 관점에서 볼 수 있는데, 필자가 느낀 대표적인 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하위&amp;nbsp;레이어&amp;nbsp;독립성&lt;/li&gt;
&lt;li&gt;생명주기 관점&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 뜻인지 상세히 설명하면,&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하위 레이어 독립성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(필자가 만든 말이니 검색해도 안나올 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서 아예 FastAPI 의존성을 없앤다는 의미이다. 즉, fastapi를 일체 import하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;서비스가 FastAPI를 모르므로 다른 곳(배치, 스크립트)에서 재사용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령, 스케줄러와 api, 워커 등 다양한 환경을 한 프로젝트에서 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;api가 아닌 다른 컨테이너는 fastapi가 필요없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;개인적으로 이렇게 코드베이스 공유하면서 간단하게 쓸 수 있는게 Python의 치트키같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776064565537&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;같은 이미지(또는 같은 코드베이스)
- api 컨테이너        &amp;rarr; uvicorn main:app
- worker 컨테이너     &amp;rarr; celery -A tasks worker
- scheduler 컨테이너  &amp;rarr; python jobs/daily_cleanup.py
- migration 컨테이너  &amp;rarr; alembic upgrade&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depends를 router에서 받으면 요청마다 세션을 만들고, 응답 후 자동으로 닫아줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 트랜잭션과 요청의 경계가 동일하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;잠깐, 그럼 응답 전까지 Connection Pool을 그대로 점유한다고요?&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;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;영속성 컨텍스트가 너무 오래 살아있는거 아닌가? &lt;/span&gt;세션의 수명을 그대로 끌고가면 트래픽이 몰리는 경우 어떻게 대응하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 그렇다. 다만, 커넥션을 View 영역까지 끌고가는 문제부터 말해보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fastapi는 Eventloop 환경 때문에 비동기로 db 세션을 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, SQLAlchemy는 기본이 lazy load이므로 비동기에서 문제가 자주 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;u&gt;명시적으로 eager load를 강제하는 경우가 많으므로 뷰에서 N+1 문제가 발생하는 경우는 줄어&lt;/u&gt;들긴 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더불어 &lt;u&gt;DTO로 즉시 반환&lt;/u&gt;하도록 하면 lazy load가 불가해지므로 대응 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 앞서 말한 Connection Pool 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 방법이 있는데 SessionFactory을 주입해서 아예 세션을 Service에서 열고 닫는 방법도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;이러면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;응답 직렬화 시점엔 커넥션이 풀로 돌아간다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776063463371&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 세션 팩토리만 주입
@router.get(&quot;/users/{id}&quot;)
async def get_user(id: int, session_factory = Depends(get_session_factory)):
    return await user_service.get(session_factory, id)

# service
async def get(session_factory, id):
    async with session_factory() as session:
        async with session.begin():
            user = await repo.find(session, id)
    # 여기서 이미 세션/커넥션 반납됨
    return UserDTO.from_orm(user)  # 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;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;근데 그 정도로 코어 시스템이면 그냥 SpringBoot 쓰는 것도 답인 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 이렇게 불편하게 살아요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 아예 다음과 같이 서비스도 Depends로 묶는다. 꽤 흔한 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;router의 인자에 DBSession 대신 UserServiceDep이 들어가긴 하나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service의 각 메서드에서 불필요한 db session을 입력할 필요가 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1776063909028&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def get_user_service(session: AsyncSessionDep) -&amp;gt; UserService:
    &quot;&quot;&quot;Get UserService instance.&quot;&quot;&quot;
    return UserService(session)

UserServiceDep = Annotated[UserService, Depends(get_user_service)]

@router.post(
    &quot;/&quot;,
    response_model=UserResponse,
    status_code=status.HTTP_201_CREATED,
    summary=&quot;사용자 생성&quot;,
)
async def create_user(
    data: UserCreate,
    service: UserServiceDep,
) -&amp;gt; UserResponse:
    &quot;&quot;&quot;새로운 사용자 생성&quot;&quot;&quot;
    user = await service.create_user(data)
    return UserResponse.model_validate(user)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserService의 __init__에서 session만 입력받아주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1776064059916&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class UserService:
    def __init__(self, session: AsyncSession):
        self.repository = UserRepository(session)

    async def create_user(self, data: UserCreate) -&amp;gt; User:
        ...생략...
        return await self.repository.create(user)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우터 등록 패턴&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# app/main.py
from app.domain.payment.router import router as payment_router
from app.domain.account.router import router as account_router
from app.domain.transfer.router import router as transfer_router
from app.domain.auth.router import router as auth_router
from app.domain.admin.router import router as admin_router

# 버전별 그룹핑
app.include_router(payment_router, prefix=&quot;/api/v1&quot;)
app.include_router(account_router, prefix=&quot;/api/v1&quot;)
app.include_router(transfer_router, prefix=&quot;/api/v1&quot;)
app.include_router(auth_router, prefix=&quot;/api/v1&quot;)

# 관리자 API는 별도 prefix
app.include_router(admin_router, prefix=&quot;/api/admin&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Pydantic&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 들어가기 전에 기초 정도는 짚고 넘어가자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fastapi를 사용하면 Pydantic 라이브러리가 자주 언급된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pydantic은&amp;nbsp;Python에서&amp;nbsp;데이터&amp;nbsp;검증&amp;nbsp;+&amp;nbsp;타입&amp;nbsp;기반&amp;nbsp;모델링을&amp;nbsp;자동으로&amp;nbsp;처리해주는&amp;nbsp;라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python은 기본적으로 타입이&amp;nbsp;느슨하다.&lt;/p&gt;
&lt;pre id=&quot;code_1776058616979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def func(age):
    return age + 1

func(&quot;10&quot;)  # 에러 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 자동으로 처리해주는 라이브러리라고 보면 된다. 기본적으로 BaseModel을 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시를 보면 age를 string 형태로 넣었으나, 자동으로 정수형으로 인식한 것을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1776058672497&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    
user = User(name=&quot;kim&quot;, age=&quot;20&quot;)
print(user.age)  # 20 (자동 변환됨)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증도 매우 간편한데 다음과 같은 검증도 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gt(Greater Than, 초과), ge(Greater Than or Equal, 이상), lt(Less Than, 미만), le(Less Than or Eqaul, 이하)&lt;/p&gt;
&lt;pre id=&quot;code_1776059028207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pydantic import BaseModel, Field

class User(BaseModel):
    age: int = Field(gt=0)
    
User(age=-1) # 에러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 변환도 간편하다. 딕셔너리 형태를 Unpacking해서 인자로 전달할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS로 치면 destructing, SpringBoot로 치면 Jackson의 Mapping 느낌?&lt;/p&gt;
&lt;pre id=&quot;code_1776059138016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data = {&quot;name&quot;: &quot;kim&quot;, &quot;age&quot;: 20}

user = User(**data)
print(user.model_dump())  # dict 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈치 챘겠지만 Fastapi의 api 호출에 활용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1776059422073&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from fastapi import FastAPI

app = FastAPI()

@app.post(&quot;/user&quot;)
def create_user(user: User):
    return user&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Depends&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depends또한 중요한 핵심 개념으로, 모르면 이해하기 힘들 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depends는 DI(의존성 주입)을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 경우, get_user에 대한 반환값을 user에 대입해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1776064843950&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def get_user():
    return {&quot;name&quot;: &quot;Lee&quot;}

@app.get(&quot;/&quot;)
def read_root(user = Depends(get_user)):
    return user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 넣어준다면 그냥 함수를 호출하면 될텐데? 왜 쓰는걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 이유는 생명주기 관리이다. Depends 없이 직접 DB세션을 호출하는 경우를 보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고: Fastapi에서 DB 연결은 비동기로 처리된다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1776065836570&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@router.get(&quot;/users/{id}&quot;)
async def get_user(id: int):
    session = await get_session()  # 근데 이거 언제 닫지?
    ...
    await session.close()  # 매번 try/finally로 감싸야 함&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depends는 단순히 함수 결과를 주입하는 게 아니라, 컨텍스트 매니저를 관리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1776066208905&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def get_session():
    async with SessionLocal() as session:
        yield session
    # yield 이후: 요청 끝나면 FastAPI가 자동으로 여기로 돌아와 정리

@router.get(&quot;/users/{id}&quot;)
async def get_user(id: int, session = Depends(get_session)):
    ...  # 예외 터져도 세션 정리됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 의존성을 자동으로 처리하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 경와 같이 요청 내에서 get_session이 여러 번 필요해도 한 번만 호출하고 캐시한다.&lt;/p&gt;
&lt;pre id=&quot;code_1776066264153&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def get_session():
    async with SessionLocal() as session:
        yield session

async def get_user_repo(session = Depends(get_session)):
    return UserRepo(session)

async def get_post_repo(session = Depends(get_session)):
    return PostRepo(session)

async def get_current_user(
    token: str = Header(...),
    session = Depends(get_session),
) -&amp;gt; User:
    return await session.get(User, decode(token))

@router.post(&quot;/posts&quot;)
async def create_post(
    body: PostCreate,
    user: User = Depends(get_current_user),
    user_repo: UserRepo = Depends(get_user_repo),
    post_repo: PostRepo = Depends(get_post_repo),
):
    ...&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;더불어 get_current_user가 없다면 매 첫 줄마다 다음을 입력했어야 한다. 심지어 OpenAPI 스펙에도 반영되지 않는다.&lt;/p&gt;
&lt;pre id=&quot;code_1776066657500&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;user = await current_user(request.headers[&quot;authorization&quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Annotated&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 맨날 귀찮게 이렇게 써야할까?&lt;/p&gt;
&lt;pre id=&quot;code_1776068820156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;session: AsyncSession = Depends(get_session)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 사전에 정의해두고 재활용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1776068852140&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SessionDep = Annotated[AsyncSession, Depends(get_session)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세상 깔끔하다. 이제 이렇게 표현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1776068942130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SessionDep = Annotated[AsyncSession, Depends(get_session)]
CurrentUser = Annotated[User, Depends(get_current_user)]

# routers/users.py
@router.get(&quot;/users/{id}&quot;)
async def get_user(id: int, session: SessionDep, user: CurrentUser):
    ...

@router.delete(&quot;/users/{id}&quot;)
async def delete_user(id: int, session: SessionDep, user: CurrentUser):
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;다음을 진행하기 전에&lt;/h1&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Pydantic, Depends 심화&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leestana01.tistory.com/25&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leestana01.tistory.com/25&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776071487904&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;FastAPI 기본부터 극한까지 날먹하기 (Pydantic, Depends 심화)&quot; data-og-description=&quot;Discriminated Union만약 결제 수단(카드, 계좌이체, 간편결제)에 따라 요청 구조가 다르다면?Spring에서는 @JsonTypeInfo + @JsonSubTypes를 사용해야 하고, 커스텀 디시리얼라이저를 작성해야 하는 경우도 많&quot; data-og-host=&quot;leestana01.tistory.com&quot; data-og-source-url=&quot;https://leestana01.tistory.com/25&quot; data-og-url=&quot;https://leestana01.tistory.com/25&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nSI2k/dJMb8Z3sAmJ/QJ441M55cxR53lQr5cGFv0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/Epr6H/dJMb8SXy3IZ/K34FjJvvapB178hqkWJaO1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/0wXCw/dJMb84p9XcJ/GlKmkkBZ7NKGCXn2VbTRbk/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600&quot;&gt;&lt;a href=&quot;https://leestana01.tistory.com/25&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leestana01.tistory.com/25&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nSI2k/dJMb8Z3sAmJ/QJ441M55cxR53lQr5cGFv0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/Epr6H/dJMb8SXy3IZ/K34FjJvvapB178hqkWJaO1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/0wXCw/dJMb84p9XcJ/GlKmkkBZ7NKGCXn2VbTRbk/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600');&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;FastAPI 기본부터 극한까지 날먹하기 (Pydantic, Depends 심화)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Discriminated Union만약 결제 수단(카드, 계좌이체, 간편결제)에 따라 요청 구조가 다르다면?Spring에서는 @JsonTypeInfo + @JsonSubTypes를 사용해야 하고, 커스텀 디시리얼라이저를 작성해야 하는 경우도 많&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leestana01.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SpringBoot 개발자용&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SpringBoot 에서 넘어왔다면 다음을 먼저 읽어보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java와 Python 그리고 SpringBoot와 Fastapi를 비교하여 개념을 정리했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것만 읽어도 FastAPI가 훨씬 쉬워질 것이다.&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://leestana01.tistory.com/24&quot;&gt;https://leestana01.tistory.com/24&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776071515448&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;FastAPI 기본부터 극한까지 파보자 (SpringBoot에서 넘어가기)&quot; data-og-description=&quot;용어부터 알아보자SpringBoot와 FastAPI는 어떻게 다를까? 기본적인 구조부터 잡고 가면 편할 것 같다.JDK (Java 17/21)Python (3.11/3.12/3.13)Maven / GradlePoetry / uv / pipapplication.yml.env + pydantic-settingsTomcat (내장 서&quot; data-og-host=&quot;leestana01.tistory.com&quot; data-og-source-url=&quot;https://leestana01.tistory.com/24&quot; data-og-url=&quot;https://leestana01.tistory.com/24&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/s1ARj/dJMb8Qens3h/rhfiTkWqIa4pdeEffTgU61/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cmXu4f/dJMb8RRS91Y/qzWW7PF2uP2gknZ4qL9U81/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ynCGy/dJMb8RRS91X/ZhcAVFEBnUOTy9xO4p2EO0/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600&quot;&gt;&lt;a href=&quot;https://leestana01.tistory.com/24&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://leestana01.tistory.com/24&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/s1ARj/dJMb8Qens3h/rhfiTkWqIa4pdeEffTgU61/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cmXu4f/dJMb8RRS91Y/qzWW7PF2uP2gknZ4qL9U81/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/ynCGy/dJMb8RRS91X/ZhcAVFEBnUOTy9xO4p2EO0/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600');&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;FastAPI 기본부터 극한까지 파보자 (SpringBoot에서 넘어가기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;용어부터 알아보자SpringBoot와 FastAPI는 어떻게 다를까? 기본적인 구조부터 잡고 가면 편할 것 같다.JDK (Java 17/21)Python (3.11/3.12/3.13)Maven / GradlePoetry / uv / pipapplication.yml.env + pydantic-settingsTomcat (내장 서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;leestana01.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;아키텍처 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 프로젝트가 무거워질수록, Facade 패턴이 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI에서는 어떤 아키텍처 구조가 좋을까?&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring에서 Facade가 자연스러운 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 Facade 패턴이 자연스럽게 작동하는 데에는 프레임워크의 근본적인 구조가 뒷받침된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, Spring IoC Container가 싱글톤 빈을 관리한다. 무거운 Service 객체들이 애플리케이션 생명주기 동안 한 번만 생성되므로, 여러 Service를 주입받아 조합하는 Facade 클래스가 비용 없이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 @Transactional도 AOP 프록시로 동작한다. Facade 메서드에 @Transactional을 선언하면 내부에서 호출하는 모든 Repository 작업이 하나의 트랜잭션으로 묶인다. 이 선언적 트랜잭션 관리가 Facade의 오케스트레이션과 궁합이 맞는다.&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;FastAPI에서 Facade가 어색한 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python은&amp;nbsp;근본적으로&amp;nbsp;다른&amp;nbsp;언어이다.&amp;nbsp;우선,&amp;nbsp;함수가&amp;nbsp;일급&amp;nbsp;객체이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직 오케스트레이션을 위해 클래스를 만들 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async def execute_transfer(...)라는 함수 하나가 오케스트레이터 역할을 완벽히 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Spring의 싱글톤 빈과 달리, FastAPI의 Depends()는 매 요청마다 새 인스턴스를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 중요한건, AOP가 없다. Python에는 @Transactional 같은 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데코레이터로 유사하게 구현할 수 있지만, Spring AOP처럼 프록시 기반이 아니라 함수 래핑이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 관점에서 바라봐도 Python에서는 파일 하나에 관련 함수를 모아두는 것이 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java: 패키지 &amp;rarr; 클래스 &amp;rarr; 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python: 패키지 &amp;rarr; 모듈(파일) &amp;rarr; 함수/클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Facade가 복잡한 비즈니스 로직을 해결하는 본질적 문제는 FastAPI에서도 당연히 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 해결 형태가 다르다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Use Case패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Use Case의 원칙&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 비즈니스 시나리오만 담당&lt;/li&gt;
&lt;li&gt;도메인 로직은 Domain Service에 위임&lt;/li&gt;
&lt;li&gt;인프라 호출(DB, 외부 API)은 Repository/Client에 위임&lt;/li&gt;
&lt;li&gt;Use Case 자체는 &quot;흐름 조율&quot;만 담당&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취소나 정산은 별도 Use Case.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도메인 별 디렉토리 구조&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;app/domain/transfer/
├── __init__.py
├── router.py                    # 라우터 (= Controller)
├── models.py                    # ORM 엔티티 (= JPA Entity)
├── schemas.py                   # 요청/응답 DTO (= Request/Response DTO)
├── enums.py                     # 상태 enum
├── repository.py                # 데이터 접근 (= Repository)
├── exceptions.py                # 도메인 예외
├── use_cases/                   # &amp;larr; Facade 대신 이것
│   ├── __init__.py
│   ├── execute_transfer.py      # 이체 실행
│   ├── cancel_transfer.py       # 이체 취소
│   └── get_transfer_status.py   # 이체 상태 조회 (단순 조회는 함수로도 충분)
└── services/                    # 도메인 서비스 (순수 비즈니스 로직)
    ├── __init__.py
    ├── fee_calculator.py
    └── limit_checker.py&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;UseCase 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제를 통해 execute_transfer.py에 대한 Use Case 를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통상 Use Case는 Class로 선언한 뒤, Annotated 타입으로 깔끔하게 호출한다.&lt;/p&gt;
&lt;pre id=&quot;code_1776068546638&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/domain/transfer/use_cases/execute_transfer.py

from dataclasses import dataclass
from sqlalchemy.ext.asyncio import AsyncSession

from app.domain.transfer.schemas import TransferCommand, TransferResult
from app.domain.transfer.services.fee_calculator import FeeCalculator
from app.domain.account.repository import AccountRepository
from app.infrastructure.notification.service import NotificationService

@dataclass(frozen=True)
class ExecuteTransferUseCase:
    &quot;&quot;&quot;
    이체 실행 UseCase.
    &quot;&quot;&quot;
    db: AsyncSession
    transfer_repo: TransferRepository
    account_repo: AccountRepository
    fee_calculator: FeeCalculator
    notification: NotificationService

    async def execute(self, command: TransferCommand) -&amp;gt; TransferResult:
        ...이체 시도 로직...
        return TransferResult.from_entity(transfer)


# DI 팩토리
from fastapi import Depends
from app.config.database import get_db

def get_execute_transfer_use_case(
    db: AsyncSession = Depends(get_db),
    ...레포 및 서비스 선언...
) -&amp;gt; ExecuteTransferUseCase:
    return ExecuteTransferUseCase(
        db=db,
        ...레포 및 서비스 초기화...
    )

# Annotated 타입으로 깔끔하게
ExecuteTransfer = Annotated[ExecuteTransferUseCase, Depends(get_execute_transfer_use_case)]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Router에서 사용&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# app/domain/transfer/router.py
from fastapi import APIRouter, Depends, Header, status
from app.domain.transfer.schemas import TransferCommand, TransferResult
from app.domain.transfer.use_cases.execute_transfer import ExecuteTransfer
from app.domain.transfer.use_cases.cancel_transfer import CancelTransfer
from app.core.security import AuthUser

router = APIRouter(prefix=&quot;/transfers&quot;, tags=[&quot;Transfers&quot;])

@router.post(&quot;/&quot;, status_code=status.HTTP_201_CREATED)
async def execute_transfer(
    command: TransferCommand,
    use_case: ExecuteTransfer,
    user: AuthUser,
) -&amp;gt; TransferResult:
    &quot;&quot;&quot;이체 실행&quot;&quot;&quot;
    command.idempotency_key = idempotency_key
    command.sender_user_id = user.user_id
    return await use_case.execute(command)

@router.post(&quot;/{transfer_id}/cancel&quot;)
async def cancel_transfer(
    transfer_id: UUID,
    use_case: CancelTransfer,
    user: AuthUser,
) -&amp;gt; TransferResult:
    &quot;&quot;&quot;이체 취소&quot;&quot;&quot;
    return await use_case.execute(transfer_id, reason, user.user_id)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단순 CRUD는 함수형으로&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 것을 Use Case 클래스로 만들 필요는 없다. 단순 조회는 함수로 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 모든 것을 Use Case로 만드는건 오버엔지니어링일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# app/domain/transfer/use_cases/get_transfer_status.py

from uuid import UUID
from fastapi import Depends
from app.domain.transfer.repository import TransferRepository
from app.domain.transfer.schemas import TransferResponse
from app.domain.transfer.exceptions import TransferNotFoundError

async def get_transfer_status(
    transfer_id: UUID,
    repo: TransferRepository = Depends(get_transfer_repo),
) -&amp;gt; TransferResponse:
    transfer = await repo.find_by_id(transfer_id)
    if not transfer:
        raise TransferNotFoundError(transfer_id)
    return TransferResponse.model_validate(transfer)

# 라우터에서 함수를 직접 호출하거나, 더 간단하게:
@router.get(&quot;/{transfer_id}&quot;)
async def get_transfer(
    transfer_id: UUID,
    repo: TransferRepository = Depends(get_transfer_repo),
    user: AuthUser,
) -&amp;gt; TransferResponse:
    transfer = await repo.find_by_id(transfer_id)
    if not transfer:
        raise TransferNotFoundError(transfer_id)
    return TransferResponse.model_validate(transfer)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 프로젝트 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 다음과 같이 프로젝트 구조를 잡아 확장 가능하도록 설계를 마련했다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;프로젝트/
├── pyproject.toml                 # = build.gradle
├── alembic/                       # = DB 마이그레이션
│   ├── alembic.ini
│   └── versions/
├── app/
│   ├── __init__.py
│   ├── main.py                    # = 진입점
│   ├── config/                    # = 설정 폴더
│   │   ├── __init__.py
│   │   ├── settings.py            # = 기본 세팅
│   │   ├── database.py            # = DataSource 설정
│   │   ├── redis.py               # = RedisConfig
│   │   └── logging.py             # = 로깅 설정
│   │
│   ├── core/                      # = 공통 인프라
│   │   ├── __init__.py
│   │   ├── exceptions.py          # = @ControllerAdvice + 커스텀 예외
│   │   ├── security.py            # = SecurityConfig
│   │   ├── middleware.py          # = Filter/Interceptor
│   │   └── dependencies.py        # = 공통 DI 팩토리
│   │
│   ├── domain/                    # = 도메인 레이어
│   │   ├── 특정도메인/               # = 특정 도메인 패키지
│   │   │   ├── __init__.py
│   │   │   ├── router.py          # = API 스펙
│   │   │   ├── service.py         # = 기본적인 스펙
│   │   │   ├── repository.py      # = DB 연동 스펙
│   │   │   ├── models.py          # = DB 모델(Entity) 구현
│   │   │   ├── schemas.py         # = DTO 구현
│   │   │   ├── enums.py           # = 상태 ENUM
│   │   │   └── exceptions.py      # = DomainNotFoundException 등
│   │   │
│   │   └── user/
│   │       └── ...
│   │
│   └── infrastructure/            # = 외부 시스템 연동
│       ├── __init__.py
│       ├── notification/          # = 알림 서비스
│       │   ├── sms.py
│       │   ├── push.py
│       │   └── email.py
│       ├── pg_gateway/            # = PG사 연동
│       │   └── ...
│       └── cache/
│           └── redis_client.py
│
├── tests/
│   ├── conftest.py                # = 공통 테스트
│   ├── unit/
│   │   ├── ...
│   ├── integration/
│   │   ├── ...
│   └── e2e/
│       └── ...
│
├── docker/                        # 귀찮으면 루트에 둠
│   ├── Dockerfile
│   └── docker-compose.yml
│
├── scripts/
│   └── ...
│
└── docs/
    └── api-spec.md&lt;/code&gt;&lt;/pre&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;아키텍처 구조&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 페이지에 다 담으려고 했는데, 게시글이 너무 길다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 각각에 대한 구현 방식은 차후 게시글을 따로 작성하여 여기에 링크하겠다.&lt;/p&gt;</description>
      <category>백엔드/FastAPI</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/23</guid>
      <comments>https://leestana01.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 30 Mar 2026 17:58:00 +0900</pubDate>
    </item>
    <item>
      <title>KVM Netfilter 설정</title>
      <link>https://leestana01.tistory.com/22</link>
      <description>&lt;h1&gt;KVM Machine Netfilter 추가 설정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KVM 머신이 외부와 통신을 하는 상황에서, Bridge 설정 및 포트포워딩을 완료해도 &lt;b&gt;불안정한 상황이 발생&lt;/b&gt;하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 Host 측에서 Netfilter를 비활성화하였으나, 간헐적으로 통신이 안되는 상황이 발생하였기에 &lt;b&gt;VM 측의 Netfilter를 점검&lt;/b&gt;했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VM의 Bridge Netfilter 비활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점검 결과는 다음과 같았다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;$ cat /proc/sys/net/bridge/bridge-nf-call-arptables
1
$ cat /proc/sys/net/bridge/bridge-nf-call-iptables
1
$ cat /proc/sys/net/bridge/bridge-nf-call-ip6tables
1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 1이면 netfilter가 활성화 상태이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KVM Bridge에서 netfilter를 비활성화하고 런타임을 변경한다.&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;echo 0 | sudo tee /proc/sys/net/bridge/bridge-nf-call-arptables
echo 0 | sudo tee /proc/sys/net/bridge/bridge-nf-call-iptables
echo 0 | sudo tee /proc/sys/net/bridge/bridge-nf-call-ip6tables&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신이 재부팅되면 /proc/sys/ 내의 설정이 초기화되므로, /etc/sysctl.conf에 영구 설정을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;sudo tee -a /etc/sysctl.conf &amp;lt;&amp;lt;EOF
net.bridge.bridge-nf-call-ip6tables = 0
net.bridge.bridge-nf-call-iptables = 0
net.bridge.bridge-nf-call-arptables = 0
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 다시 로드한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;sudo sysctl -p /etc/sysctl.conf&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;init 프로그램이 /etc/sysctl.conf를 불러온다.&lt;/li&gt;
&lt;li&gt;/etc/rc.d/rc.sysinit 스크립트가 sysctl을 실행하여 설정을 적용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 SpringBoot의 간헐적 통신 불가능 문제는 바뀌지 않았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 문헌&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.linux-console.net/?p=22149&quot;&gt;Linux Console - Netfilter 설정&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;KVM Machine Netfilter 추가 설정 2&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host의 브릿지 넷필터를 비활성화 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브릿지 넷필터 비활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/etc/sysctl.d/bridge-filter.conf로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;net.bridge.bridge-nf-call-ip6tables=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/etc/udev/rules.d/99-bridge-filter.rules로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ACTION==&quot;add&quot;, SUBSYSTEM==&quot;module&quot;, KERNEL==&quot;br_netfilter&quot;, RUN+=&quot;/sbin/sysctl -p /etc/sysctl.d/bridge-filter.conf&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;netfilter 모듈이 적재될 때 자동으로 위의 sysctl 설정을 적용하는 udev 규칙이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 문헌&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://osg.kr/archives/532&quot;&gt;OSG - 브릿지 넷필터 비활성화&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/KVM(Hypervisor Type1.5)</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/22</guid>
      <comments>https://leestana01.tistory.com/22#entry22comment</comments>
      <pubDate>Sat, 28 Mar 2026 22:34:59 +0900</pubDate>
    </item>
    <item>
      <title>KVM Hypervisor Type 1.5에서 VM 관리</title>
      <link>https://leestana01.tistory.com/21</link>
      <description>&lt;h1&gt;KVM[HyperVisor Type1] 설치&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu Server 22.04 CLI 환경&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KVM 지원 여부 확인&lt;/h2&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;grep -Eoc '(vmx|svm)' /proc/cpuinfo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과가 &lt;b&gt;0이 아니면&lt;/b&gt; KVM을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KVM을 지원하지 않는 경우, BIOS 설정 문제일 가능성도 높다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HP 기준 해결 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Security&lt;/code&gt; &amp;gt; &lt;code&gt;System Security&lt;/code&gt; &amp;gt; &lt;code&gt;Virtualization Technology (VTx)&lt;/code&gt; 를 &lt;b&gt;Enabled&lt;/b&gt;로 설정&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패키지 설치&lt;/h2&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패키지명&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qemu-kvm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;KVM 하이퍼바이저용 하드웨어 에뮬레이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libvirt-daemon-system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;libvirt 데몬 시스템 서비스 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libvirt-clients&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가상화 플랫폼 관리 소프트웨어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge-utils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이더넷 브리지 구성 명령 줄 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;virtinst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가상 머신 생성 명령 줄 도구&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;virt-manager를 제외한 이유&lt;/b&gt;&lt;br /&gt;CLI 환경에서만 작업하므로, virt-manager는 사용이 제한됨. 따라서 설치 대상에서 제외&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 완료 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kvm 설치 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ kvm --version
QEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.15)
Copyright (c) 2003-2021 Fabrice Bellard and the QEMU Project developers&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 후 추가 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그룹에 사용자 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;sudo usermod -aG libvirt $USER
sudo usermod -aG kvm $USER&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 시작 및 활성화&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;sudo systemctl enable --now libvirtd&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;게스트 OS 생성&lt;/h2&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;virt-install \
  --name [가상머신 이름] \
  --ram [메모리 크기] \
  --disk path=[디스크 경로],size=[디스크 크기] \
  --vcpus [CPU 수] \
  --os-type [OS 타입] \
  --os-variant [OS 버전] \
  --network bridge=virbr0 \
  --graphics none \
  --console pty,target_type=serial \
  --location '[OS 설치 이미지 경로]' \
  --extra-args 'console=ttyS0,115200n8 serial'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;--network bridge=virbr0&lt;/code&gt; : 가상 머신이 호스트 시스템의 virbr0 브리지로 연결되도록 함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--graphics none&lt;/code&gt; : CLI 환경이므로 그래픽 출력 비활성화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--extra-args 'console=ttyS0,115200n8 serial'&lt;/code&gt; : 해당 머신의 화면 정보를 시리얼 콘솔로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;sudo virt-install \
  --name myvm \
  --vcpus 2 \
  --ram 2048 \
  --os-variant ubuntu20.04 \
  --location 'http://archive.ubuntu.com/ubuntu/dists/focal/main/installer-amd64/' \
  --network bridge=virbr0,model=virtio \
  --graphics none \
  --extra-args='console=ttyS0,115200n8 serial'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기타 게스트 OS 관리&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;virsh start [가상머신 이름]      # 시작
virsh shutdown [가상머신 이름]   # 정지
virsh reboot [가상머신 이름]     # 재시작
virsh list --all                 # 게스트 상태 확인&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;KVM에서 VM 생성 (접속 불가 오류 해결 포함)&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KVM으로 VM을 생성하면 &lt;span style=&quot;background-color: #dddddd; color: #000000;&quot;&gt;&amp;nbsp;Escape character is ^] (Ctrl + ])&amp;nbsp;&lt;/span&gt; 이후에 진행이 안되는 문제가 발생한다.&lt;br /&gt;해외 사이트에도 방안이 나와있지 않아 직접 연구하여 서술한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VM 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 참고하여 VM을 생성한다. 현재 생성한 방법은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;name&lt;/b&gt; : &lt;code&gt;DNSserv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;disk&lt;/b&gt; : 논리 디스크 경로 (LVM 파티션을 따로 생성하였음)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;virt-install \
  --name DNSserv \
  --ram 2048 \
  --vcpus 2 \
  --os-variant ubuntu20.04 \
  --disk path=/dev/ubuntu-vg/kvmvol1,bus=virtio,size=10 \
  --network bridge=virbr0,model=virtio \
  --graphics none \
  --location 'http://archive.ubuntu.com/ubuntu/dists/focal/main/installer-amd64/' \
  --extra-args 'console=ttyS0,115200n8 serial'&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 &amp;mdash; Logical Volume 생성&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo vgscan                                          # Volume Group 확인
sudo vgdisplay 해당그룹                               # 여유 공간 확인
sudo lvcreate -n 새볼륨명 -L 10G 해당그룹              # 새 논리 볼륨 생성
sudo mkfs.ext4 /dev/해당그룹/새볼륨명                   # 파일 시스템 생성&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 진행&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQPpx/dJMcabp7ZV2/e23KASk7o2UoGgKylpMpK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQPpx/dJMcabp7ZV2/e23KASk7o2UoGgKylpMpK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQPpx/dJMcabp7ZV2/e23KASk7o2UoGgKylpMpK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQPpx%2FdJMcabp7ZV2%2Fe23KASk7o2UoGgKylpMpK1%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;649&quot; height=&quot;362&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Go Back&lt;/code&gt; 선택&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXbPZk/dJMcafzhUFk/ll61Xn5lNrY5AQU6iTFpN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXbPZk/dJMcafzhUFk/ll61Xn5lNrY5AQU6iTFpN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXbPZk/dJMcafzhUFk/ll61Xn5lNrY5AQU6iTFpN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXbPZk%2FdJMcafzhUFk%2Fll61Xn5lNrY5AQU6iTFpN1%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;633&quot; height=&quot;366&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Download installer components&lt;/code&gt; 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 설정을 진행한다. 설치를 위한 기본 필드이므로, 안내에 따라 진행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hostname&lt;/li&gt;
&lt;li&gt;Ubuntu Archive 미러 도메인 설정&lt;/li&gt;
&lt;li&gt;소유주명&lt;/li&gt;
&lt;li&gt;계정명 / 계정 비밀번호 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 마치면 다시 처음 본 창이 뜬다. &lt;code&gt;Go back&lt;/code&gt; 선택&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH 설정을 위한 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI 환경에서 접속 불가한 VM 머신에 연결하도록 &lt;b&gt;미리 OpenSSH Server를 구동&lt;/b&gt;시킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN4X52/dJMcabKrlsE/Kk5zXwQYBvaUoYk7aJLuk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN4X52/dJMcabKrlsE/Kk5zXwQYBvaUoYk7aJLuk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN4X52/dJMcabKrlsE/Kk5zXwQYBvaUoYk7aJLuk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN4X52%2FdJMcabKrlsE%2FKk5zXwQYBvaUoYk7aJLuk1%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;637&quot; height=&quot;389&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Select and install software&lt;/code&gt; 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;파티션 설정&lt;/b&gt; 및 &lt;b&gt;PAM 설정&lt;/b&gt;을 진행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 &amp;mdash; 파티션 설정 선택지&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택지&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Guided - resize Virtual disk 1 (vda) and use freed space&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기존 OS/데이터가 있는 디스크에서 추가 공간을 만들 때 유용. KVM 인스턴스에는 비적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Guided - use entire disk&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;디스크 전체 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Guided - use entire disk and set up LVM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;전체 디스크 사용 + LVM 설정. 추후 파티션 크기 조정&amp;middot;스냅샷 등 필요 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Guided - use entire disk and set up encrypted LVM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;LVM + 디스크 전체 암호화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Manual&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;수동 파티셔닝. 파티션 크기&amp;middot;파일 시스템 유형&amp;middot;마운트 포인트 등 직접 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 &amp;mdash; PAM(자동 업데이트) 설정 선택지&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선택지&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;No automatic updates&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모든 업데이트를 수동으로 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Install security updates automatically&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;보안 업데이트만 자동 설치. 취약점에 신속 대응&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Manage system with Landscape&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Canonical의 Landscape를 통해 웹에서 중앙 관리. 대규모 배포에 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzAUer/dJMcabKrlsJ/EJ9SY5c9CHDXz39ho11NqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzAUer/dJMcabKrlsJ/EJ9SY5c9CHDXz39ho11NqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzAUer/dJMcabKrlsJ/EJ9SY5c9CHDXz39ho11NqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzAUer%2FdJMcabKrlsJ%2FEJ9SY5c9CHDXz39ho11NqK%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;644&quot; height=&quot;392&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 창이 뜨면 다음을 선택하여 설치한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OpenSSH server&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Basic Ubuntu server&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;GRUB 부트 로더&lt;/b&gt;를 설치한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 완료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cf0QzW/dJMcabKrlsM/Bto91j2vAjQZzcldTQt9kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cf0QzW/dJMcabKrlsM/Bto91j2vAjQZzcldTQt9kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cf0QzW/dJMcabKrlsM/Bto91j2vAjQZzcldTQt9kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcf0QzW%2FdJMcabKrlsM%2FBto91j2vAjQZzcldTQt9kK%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;628&quot; height=&quot;316&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 발생&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cohaM6/dJMcaflJ8tM/ea2iaCN9RUWquadkEOKvq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cohaM6/dJMcaflJ8tM/ea2iaCN9RUWquadkEOKvq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cohaM6/dJMcaflJ8tM/ea2iaCN9RUWquadkEOKvq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcohaM6%2FdJMcaflJ8tM%2Fea2iaCN9RUWquadkEOKvq1%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;614&quot; height=&quot;56&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막상 설치가 완료되어도 &lt;code&gt;Escape character is ^] (Ctrl + ])&lt;/code&gt; 에서 &lt;b&gt;아무 반응이 없을 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VM의 IP 확인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WA5tJ/dJMcaflJ8tN/8Lgm8F9PdzDVj05Glu7zA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WA5tJ/dJMcaflJ8tN/8Lgm8F9PdzDVj05Glu7zA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WA5tJ/dJMcaflJ8tN/8Lgm8F9PdzDVj05Glu7zA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWA5tJ%2FdJMcaflJ8tN%2F8Lgm8F9PdzDVj05Glu7zA1%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;694&quot; height=&quot;82&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결된 VM의 IP를 확인한다. 여기서는 &lt;code&gt;192.168.122.115&lt;/code&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH 접속&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNRPpF/dJMcabKrlsQ/Nw4wbcmGc4RfKVUtvn1T8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNRPpF/dJMcabKrlsQ/Nw4wbcmGc4RfKVUtvn1T8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNRPpF/dJMcabKrlsQ/Nw4wbcmGc4RfKVUtvn1T8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNRPpF%2FdJMcabKrlsQ%2FNw4wbcmGc4RfKVUtvn1T8k%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;653&quot; height=&quot;119&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;119&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCT84M/dJMcaflJ8tQ/8uwtjw0LBrYDabeMpKwe7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCT84M/dJMcaflJ8tQ/8uwtjw0LBrYDabeMpKwe7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCT84M/dJMcaflJ8tQ/8uwtjw0LBrYDabeMpKwe7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCT84M%2FdJMcaflJ8tQ%2F8uwtjw0LBrYDabeMpKwe7K%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;620&quot; height=&quot;538&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접속이 된다. 그러나 이는 &lt;b&gt;임시방편&lt;/b&gt;이므로, &lt;code&gt;virsh console&lt;/code&gt;이 작동되게 해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;grub 설정 편집&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;nano /etc/default/grub&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변경 전&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oimnj/dJMcabKrlsV/EqcjihJNY7NTMTKoNsBXw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oimnj/dJMcabKrlsV/EqcjihJNY7NTMTKoNsBXw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oimnj/dJMcabKrlsV/EqcjihJNY7NTMTKoNsBXw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOimnj%2FdJMcabKrlsV%2FEqcjihJNY7NTMTKoNsBXw0%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;620&quot; height=&quot;303&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;변경 후&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qDgEV/dJMcaiW1vKa/6fwnbxfZg5VkiPTqdE5Tl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qDgEV/dJMcaiW1vKa/6fwnbxfZg5VkiPTqdE5Tl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qDgEV/dJMcaiW1vKa/6fwnbxfZg5VkiPTqdE5Tl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqDgEV%2FdJMcaiW1vKa%2F6fwnbxfZg5VkiPTqdE5Tl0%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;613&quot; height=&quot;303&quot; data-origin-width=&quot;613&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 변경한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT=&quot;console=tty0 console=ttyS0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsxp0e/dJMcaiW1vKe/aLuVPRJ9LNKtvnhuKfyDm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsxp0e/dJMcaiW1vKe/aLuVPRJ9LNKtvnhuKfyDm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsxp0e/dJMcaiW1vKe/aLuVPRJ9LNKtvnhuKfyDm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsxp0e%2FdJMcaiW1vKe%2FaLuVPRJ9LNKtvnhuKfyDm0%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;597&quot; height=&quot;211&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQIDlw/dJMcaiW1vKf/qVT04eGJ6MZUZwkfHjz7S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQIDlw/dJMcaiW1vKf/qVT04eGJ6MZUZwkfHjz7S1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQIDlw/dJMcaiW1vKf/qVT04eGJ6MZUZwkfHjz7S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQIDlw%2FdJMcaiW1vKf%2FqVT04eGJ6MZUZwkfHjz7S1%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;530&quot; height=&quot;97&quot; data-origin-width=&quot;530&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 명령어를 순차적으로 입력한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;update-grub
systemctl enable serial-getty@ttyS0.service
systemctl start serial-getty@ttyS0.service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 쉘에서 탈출한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;접속 시도&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;virsh console 머신이름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1RGxG/dJMcaiW1vKh/3je6UvOeEDx97KXeBHjt30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1RGxG/dJMcaiW1vKh/3je6UvOeEDx97KXeBHjt30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1RGxG/dJMcaiW1vKh/3je6UvOeEDx97KXeBHjt30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1RGxG%2FdJMcaiW1vKh%2F3je6UvOeEDx97KXeBHjt30%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;604&quot; height=&quot;613&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로서 VM 생성이 완료된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;KVM Network Bridge 설정 (VM당 개별 IP 할당)&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KVM의 VM을 위해서 Bridge를 설정할 때, VM에서 Bridge 설정이 제대로 되지 않는다.&lt;br /&gt;이 역시도 해외 사이트를 검색해도 정보가 나오지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 네트워크 인터페이스 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ip a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본문에서는 &lt;code&gt;eno1&lt;/code&gt; 인터페이스를 가정하여 서술한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bridge 설정&lt;/h2&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;sudo vi /etc/netplan/파일명.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 환경&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우터 : &lt;code&gt;192.168.1.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP 대역 : &lt;code&gt;192.168.1.X&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DHCP : &lt;code&gt;192.168.1.11&lt;/code&gt; ~ &lt;code&gt;199&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VM 머신 : &lt;code&gt;192.168.1.2&lt;/code&gt; ~ &lt;code&gt;192.168.1.10&lt;/code&gt; 범위 내에서 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;eno1&lt;/code&gt; 인터페이스에 대해 bridge를 설정한다. bridge의 이름은 &lt;code&gt;br0&lt;/code&gt;으로 설정하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/weGLI/dJMcab4HBIG/3hzRNIHn78PkYG5BGvekH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/weGLI/dJMcab4HBIG/3hzRNIHn78PkYG5BGvekH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/weGLI/dJMcab4HBIG/3hzRNIHn78PkYG5BGvekH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FweGLI%2FdJMcab4HBIG%2F3hzRNIHn78PkYG5BGvekH1%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;297&quot; height=&quot;261&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 호스트의 &lt;b&gt;고정 IP 설정&lt;/b&gt;을 원한다면 다음과 같이 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yBNme/dJMcaiW1vKm/4QFoqSHtAl1MJ0PHk8hhq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yBNme/dJMcaiW1vKm/4QFoqSHtAl1MJ0PHk8hhq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yBNme/dJMcaiW1vKm/4QFoqSHtAl1MJ0PHk8hhq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyBNme%2FdJMcaiW1vKm%2F4QFoqSHtAl1MJ0PHk8hhq0%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;297&quot; height=&quot;289&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;sudo netplan apply&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로서 &lt;b&gt;호스트의 bridge 설정이 완료&lt;/b&gt;된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VM의 bridge 연결&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;virsh edit VM이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&amp;lt;devices&amp;gt;&lt;/code&gt; 태그 내에서 &lt;code&gt;&amp;lt;interface&amp;gt;&lt;/code&gt; 영역을 수정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDYVXu/dJMcaco03EA/IqiPHNt1PfqAwk4q6ue3V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDYVXu/dJMcaco03EA/IqiPHNt1PfqAwk4q6ue3V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDYVXu/dJMcaco03EA/IqiPHNt1PfqAwk4q6ue3V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDYVXu%2FdJMcaco03EA%2FIqiPHNt1PfqAwk4q6ue3V1%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;625&quot; height=&quot;80&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설정은 &lt;code&gt;nat&lt;/code&gt; 방식으로 되어있는데, &lt;code&gt;&amp;lt;interface&amp;gt;&lt;/code&gt;와 &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt;를 &lt;code&gt;bridge&lt;/code&gt;로 변경해준다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;virsh reboot VM이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로서 &lt;b&gt;VM의 bridge 설정이 완료&lt;/b&gt;된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VM 내에서 IP 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM 내부에서도 netplan 설정을 해준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;virsh console VM이름                      # VM 진입
sudo vi /etc/netplan/파일명.yaml           # netplan 설정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpjaJU/dJMcahX9vKS/K6FGE1OSJvlKQY0IUcxf31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpjaJU/dJMcahX9vKS/K6FGE1OSJvlKQY0IUcxf31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpjaJU/dJMcahX9vKS/K6FGE1OSJvlKQY0IUcxf31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpjaJU%2FdJMcahX9vKS%2FK6FGE1OSJvlKQY0IUcxf31%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;296&quot; height=&quot;161&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 DHCP로 할당받기 원한다면 &lt;code&gt;dhcp4: yes&lt;/code&gt;로 설정한다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;sudo netplan apply&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로서 &lt;b&gt;VM 내부의 bridge IP 설정이 완료&lt;/b&gt;된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버그 발생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 통신을 시도하면, VM이 Host와는 &lt;code&gt;ping&lt;/code&gt;으로 통신이 원활하나 &lt;b&gt;Router 및 외부와는 통신이 불가&lt;/b&gt;하다. 이 문제는 여러 곳에서도 보고되나 해결 방안이 제시된 곳이 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;leesh@lshserv:~$ arp -a
? (192.168.1.3) at 52:54:00:ee:5d:6e [ether] on virbr0
_gateway (192.168.1.1) at bc:62:ce:2a:91:8c [ether] on virbr0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host의 bridge 할당을 확인해보았으나 문제가 없었고, 공유기 설정에서도 VM에 IP를 제대로 할당하였다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;dnsserv@dnsserv:/etc/netplan$ sudo tcpdump -i enp1s0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp1s0, link-type EN10MB (Ethernet), capture size 262144 bytes
11:09:54.530661 STP 802.1d, Config, Flags [none], bridge-id 8000.4e:8d:53:35:f2:bf.8002, length 35
11:09:55.249151 IP 192.168.1.3.45126 &amp;gt; 8.8.8.8.domain: Flags [S], seq 2656147049, win 64240, ...
11:09:56.546801 STP 802.1d, Config, Flags [none], bridge-id 8000.4e:8d:53:35:f2:bf.8002, length 35
11:09:58.193223 IP 192.168.1.3.53194 &amp;gt; 8.8.8.8.domain: Flags [S], seq 1669409495, win 64240, ...
11:09:58.530844 STP 802.1d, Config, Flags [none], bridge-id 8000.4e:8d:53:35:f2:bf.8002, length 35&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tcpdump 확인 결과, VM이 공유기에 IP 요청을 원활히 하였으나 &lt;b&gt;응답이 없다.&lt;/b&gt; 패킷이 Host를 통해 지나가는 과정에서 Router로부터의 응답이 차단된 것으로 판단하여, &lt;b&gt;Host단의 문제&lt;/b&gt;로 가정하고 원인을 파악했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버그 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷필터 모듈 확인 :&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;lsmod | grep br_netfilter&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;50&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMARPY/dJMcaiJuo4p/BXIiKQNmJInSkDICVilbSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMARPY/dJMcaiJuo4p/BXIiKQNmJInSkDICVilbSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMARPY/dJMcaiJuo4p/BXIiKQNmJInSkDICVilbSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMARPY%2FdJMcaiJuo4p%2FBXIiKQNmJInSkDICVilbSK%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;354&quot; height=&quot;50&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;50&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브릿지 네트워크 트래픽 처리 모듈(&lt;code&gt;br_netfilter&lt;/code&gt;)이 적재되어있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 모듈을 &lt;b&gt;비활성화&lt;/b&gt;하고, 재부팅 시 &lt;b&gt;자동 적재를 방지&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;sudo rmmod br_netfilter
echo &quot;blacklist br_netfilter&quot; | sudo tee -a /etc/modprobe.d/blacklist.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;참고 문헌&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://young-cow.tistory.com/86&quot;&gt;[Linux] Ubuntu 20.04에 KVM 설치하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://ko.linux-console.net/?p=705&quot;&gt;Ubuntu 20.04에 KVM을 설치하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1; text-align: left;&quot; href=&quot;https://freelinuxtutorials.com/fixing-kvm-guest-virsh-console-hangs-at-escape-character/&quot;&gt;Fixing KVM guest virsh console hangs at Escape character&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>인프라/KVM(Hypervisor Type1.5)</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/21</guid>
      <comments>https://leestana01.tistory.com/21#entry21comment</comments>
      <pubDate>Sat, 28 Mar 2026 21:32:50 +0900</pubDate>
    </item>
    <item>
      <title>DNS 네임서버 구축 및 API를 통한 자동화 관리 올인원</title>
      <link>https://leestana01.tistory.com/20</link>
      <description>&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;설계목표&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 네임서버를 구축한다.&lt;br /&gt;- 네임서버를 API를 통해 편하게 관리한다.&lt;br /&gt;- API를 통해 DNS 레코드 및 Redirection을 관리한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 네임서버는 API를 제공하지 않는다.&lt;br /&gt;- 네임서버는 Redirection을 지원하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AI도 할 줄 모른다... 이 글 학습하면 앞으로 가능할듯&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-2&quot; data-target=&quot;heading-2&quot;&gt;DNS 조회 원리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-3&quot; data-target=&quot;heading-3&quot;&gt;DNS 서버 설정 (bind9)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-9&quot; data-target=&quot;heading-9&quot;&gt;DNS&amp;nbsp; TLD에 공개&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-12&quot; data-target=&quot;heading-12&quot;&gt;NginX&amp;nbsp;기반&amp;nbsp;리다이렉트&amp;nbsp;진행&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-15&quot; data-target=&quot;heading-15&quot;&gt;DNS 세팅 API 자동화 - Bind9 자동화&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-18&quot; data-target=&quot;heading-18&quot;&gt;DNS 세팅 API 자동화 - Nginx 자동화&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. &lt;a style=&quot;color: #8b95a1;&quot; href=&quot;https://leestana01.tistory.com/20#heading-19&quot; data-target=&quot;heading-19&quot;&gt;DNS 세팅 API 자동화 - API를 통해&amp;nbsp; DNS, Nginx 관리 자동화&lt;/a&gt; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. &lt;span style=&quot;color: #666666;&quot;&gt;&lt;a style=&quot;color: #666666;&quot; href=&quot;https://leestana01.tistory.com/20#heading-24&quot;&gt;마치며&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;DNS 조회 원리&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 이미지로 대체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 우리 네임서버(klr.kr)를 TLD에 등록하여, 쿼리 가능하도록 해야한다는 점이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2EBU7/dJMcabQ7qYK/XJkkkcLJ9PBgrRKzSyy2t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2EBU7/dJMcabQ7qYK/XJkkkcLJ9PBgrRKzSyy2t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2EBU7/dJMcabQ7qYK/XJkkkcLJ9PBgrRKzSyy2t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2EBU7%2FdJMcabQ7qYK%2FXJkkkcLJ9PBgrRKzSyy2t1%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;472&quot; height=&quot;306&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;DNS 서버 설정(Bind9)&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;목표 : DNS 서버를 구축한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;bind9 설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1774427524395&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install bind9&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;/etc/bind/named.conf.local 수정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 네임서버를 정의하고, 레코드 설정을 기록할 경로를 입력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zone 파일은 밑에서 설명하겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1774427458727&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;zone &quot;klr.kr&quot; {
    type master;
    file &quot;/etc/bind/zones/db.klr.kr&quot;; # Zone 파일 경로
};&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;Zone 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. /etc/bind/zones 폴더 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. /etc/bind/zones/db.klr.kr 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 편리한 설정을 위해 db.local 을 복사한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774427820732&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo cp db.local ./zones/db.klr.kr&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일을 열어 아래와 같이 도메인(klr.kr) 및 네임서버 ip를 일치시킨다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     ns1.klr.kr. admin.klr.kr. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
; Name Server Info
@       3600    IN      NS      ns1.klr.kr.
; Name Server A record
ns1     IN      A       203.253.76.163

@       IN      A       203.253.76.163
a       IN      CNAME   google.com.
b       IN      A       158.180.67.189&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법을 상세히 알고싶다면? 아래 더보기&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;$TTL&amp;nbsp;604800&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;: DNS 레코드가 캐시될 시간을 초단위로 지정 (1주일로 지정됨)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;상기된 레코드는 다음과 같이 분류된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;A, CNAME은 다들 알텐데, SOA와 NS는 생소할 수 있다.&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: 25%;&quot;&gt;SOA&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;NS&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;A&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;CNAME&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;도메인 기본 설정&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;네임서버 정보&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;IP 주소 안내&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&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: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;차례로 분석해보면 다음과 같다&lt;/span&gt;&lt;/p&gt;
&lt;table id=&quot;345e173b-712a-440f-b8b1-49c6e6cfe8e1&quot; style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;4bd8963b-3298-48e6-a44f-752be5f23935&quot;&gt;
&lt;td id=&quot;T;sl&quot;&gt;@&lt;/td&gt;
&lt;td id=&quot;DWsr&quot;&gt;IN&lt;/td&gt;
&lt;td id=&quot;[{Ls&quot;&gt;SOA&lt;/td&gt;
&lt;td id=&quot;?qh&amp;gt;&quot;&gt;ns1.klr.kr.&lt;/td&gt;
&lt;td id=&quot;quso&quot;&gt;admin.klr.kr.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;현재 영역 이름&lt;br /&gt;(klr.kr&lt;/td&gt;
&lt;td&gt;인터넷 클래스&lt;/td&gt;
&lt;td&gt;Start of Authority&lt;br /&gt;시작 권한 레코드&lt;/td&gt;
&lt;td&gt;주 네임서버&lt;/td&gt;
&lt;td&gt;도메인 관리자 메일 주소&lt;br /&gt;(실제로 @는 .으로 대체됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table id=&quot;51bf569a-ec81-4c27-aec2-8551d9e6d25f&quot; style=&quot;border-collapse: collapse; width: 100%; height: 190px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr id=&quot;26b8e86b-c52b-44ba-a1b8-2f0518def182&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;&amp;#96;MHg&quot; style=&quot;height: 38px;&quot;&gt;Serial&lt;/td&gt;
&lt;td id=&quot;i&amp;lt;IQ&quot; style=&quot;height: 38px;&quot;&gt;영역 파일 버전 일련번호&lt;br /&gt;DNS 설정 변경시마다 증가해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;c1ede9d3-03c8-4a6d-b5f1-332a0b9fe410&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;&amp;#96;MHg&quot; style=&quot;height: 38px;&quot;&gt;Refresh&lt;/td&gt;
&lt;td id=&quot;i&amp;lt;IQ&quot; style=&quot;height: 38px;&quot;&gt;보조 네임서버가 주 네임서버에 &lt;br /&gt;변경사항 확인하는 주기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;4c337de6-6cfb-4d7f-87ff-919afa3ecc8e&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;&amp;#96;MHg&quot; style=&quot;height: 38px;&quot;&gt;Retry&lt;/td&gt;
&lt;td id=&quot;i&amp;lt;IQ&quot; style=&quot;height: 38px;&quot;&gt;주 네임서버 연결 실패시&lt;br /&gt;재시도할 시간 간격&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;3352ef39-b16a-45b1-ba39-054b456aa4e1&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;&amp;#96;MHg&quot; style=&quot;height: 38px;&quot;&gt;Expire&lt;/td&gt;
&lt;td id=&quot;i&amp;lt;IQ&quot; style=&quot;height: 38px;&quot;&gt;보조 네임저버가 주 네임서버와&lt;br /&gt;연결하지 못할 때 정보유지 기간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;d066b8c3-82e0-49eb-9aaa-e1372e684766&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;&amp;#96;MHg&quot; style=&quot;height: 38px;&quot;&gt;Negative Cache TTL&lt;/td&gt;
&lt;td id=&quot;i&amp;lt;IQ&quot; style=&quot;height: 38px;&quot;&gt;존재하지 않는 레코드에 대한&lt;br /&gt;응답을 캐시하는 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(선택) 정의되지 않은 DNS 매핑&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 PC/서버에서 DNS를 이 네임서버로만 가리키고 싶은 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 서버에는 다른 DNS 정보가 없기 때문에 구글,네이버 등의 외부 사이트 쿼리에 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&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;/etc/bind/named.conf.options 파일에서 다음을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;forwarders {
    8.8.8.8;
    8.8.4.4;
};
allow-query { any; };&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;수정사항 반영&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1774428647043&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 방법 1(추천) - 변경사항만 반영
rndc reload 

# 방법 2 - bind9 재시작
sudo systemctl restart bind9&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DNS 테스트&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;dig a.klr.kr @localhost
dig b.klr.kr @localhost
#또는 nslookup b.klr.kr&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생한 경우? 아래 더보기&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;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;/p&gt;
&lt;pre id=&quot;code_1774428821318&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;named-checkzone [영역 이름] [영역 파일 경로]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774428846151&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo named-checkzone klr.kr /etc/bind/zones/db.klr.kr&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장 내 컴퓨터에서 네임서버에 접근 가능한지 보고 싶다면?&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;(개별 클라이언트) DNS 연동&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;별거 없다. 그냥 클라이언트 네임서버 주소 변경이다.&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;아직 TLD에 네임서버를 등록하지 않아서 외부에서는 네임서버를 알아볼 수 없다.&lt;/span&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이 방식은 임의로 클라이언트에서 네임서버를 현재 서버로 지정하여 접속 가능하게 한다.&lt;/span&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;이 설정은 개별 클라이언트에 적용되므로, 범용적이지 않다.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 1&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;neplan에서 nameserver를 공인 ip로 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 보면 알 수 있다. 별도 설명은 하지 않겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 2&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일시 변경 : sudo vi /etc/resolv.conf 에 다음을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774429170195&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nameserver [ip주소]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;영구 변경 : sudo vi /etc/resolvconf/resolv.conf.d/head&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 다음을 추가하고 재부팅한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774429182981&quot; class=&quot;apache&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;nameserver [ip주소]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이로써 매우 간단하게 네임서버 구축을 완료하였다.&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;DNS TLD에 공개&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;목표 : DNS 서버를 외부에 공개하여, 모든 클라이언트에서 DNS 쿼리를 가능하게 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설 보안망 구축이 아닌 이상, 외부에서 쿼리도 못하는 네임서버는 반쪽짜리 네임서버다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;호스트 등록&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네임서버를 TLD 수준에 등록한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNvJ9O/dJMcaiiptS6/1g0Tt8gWRrJeaZD0FcKbKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNvJ9O/dJMcaiiptS6/1g0Tt8gWRrJeaZD0FcKbKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNvJ9O/dJMcaiiptS6/1g0Tt8gWRrJeaZD0FcKbKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNvJ9O%2FdJMcaiiptS6%2F1g0Tt8gWRrJeaZD0FcKbKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;661&quot; height=&quot;253&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;네임서버 변경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록한 네임서버로 도메인을 연동한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b72gAt/dJMcab4ETCw/rAKozxw9zsLPvlUNNpVKc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b72gAt/dJMcab4ETCw/rAKozxw9zsLPvlUNNpVKc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b72gAt/dJMcab4ETCw/rAKozxw9zsLPvlUNNpVKc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb72gAt%2FdJMcab4ETCw%2FrAKozxw9zsLPvlUNNpVKc1%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;604&quot; height=&quot;326&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;438&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.com&lt;/code&gt;과 &lt;code&gt;.net&lt;/code&gt; : 3~4 시간 (실시간 루트 네임서버 변경)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.kr&lt;/code&gt; (한국인터넷진흥원) : 약 1일 소요
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전일 18:00 ~ 금일 08:00 변경 건 : 금일 08:20 업데이트&lt;/li&gt;
&lt;li&gt;금일 08:00 ~ 금일 12:00 변경 건 : 금일 12:20 업데이트&lt;/li&gt;
&lt;li&gt;금일 12:00 ~ 금일 18:00 변경 건 : 금일 18:20 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.us&lt;/code&gt; &lt;code&gt;.cn&lt;/code&gt; &lt;code&gt;.jp&lt;/code&gt; &lt;code&gt;.in&lt;/code&gt; : 약 1일 소요&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.biz&lt;/code&gt; &lt;code&gt;.info&lt;/code&gt; &lt;code&gt;.org&lt;/code&gt; : 3~4시간 (실시간 루트 네임서버 변경)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변경된 네임서버 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.whatsmydns.net/&quot;&gt;https://www.whatsmydns.net/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774429406489&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;DNS Propagation Checker - Global DNS Checker Tool&quot; data-og-description=&quot;Instant DNS Propagation Check. Global DNS Propagation Checker - Check DNS records around the world.&quot; data-og-host=&quot;www.whatsmydns.net&quot; data-og-source-url=&quot;https://www.whatsmydns.net/&quot; data-og-url=&quot;https://www.whatsmydns.net/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/y7vIA/dJMb8XR5fVI/1ArZWKRMkBsd6SSKuXVKi1/img.png?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/lrniR/dJMb87NV0PB/7OPp9yvoYblqWkatm2Ug61/img.png?width=800&amp;amp;height=260&amp;amp;face=0_0_800_260&quot;&gt;&lt;a href=&quot;https://www.whatsmydns.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.whatsmydns.net/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/y7vIA/dJMb8XR5fVI/1ArZWKRMkBsd6SSKuXVKi1/img.png?width=1600&amp;amp;height=800&amp;amp;face=0_0_1600_800,https://scrap.kakaocdn.net/dn/lrniR/dJMb87NV0PB/7OPp9yvoYblqWkatm2Ug61/img.png?width=800&amp;amp;height=260&amp;amp;face=0_0_800_260');&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;DNS Propagation Checker - Global DNS Checker Tool&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Instant DNS Propagation Check. Global DNS Propagation Checker - Check DNS records around the world.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.whatsmydns.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;Nginx 기반 리다이렉트 진행&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;목표 : 내 DNS 서버가 Redirection까지 지원하도록 설계한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만든 사이트를 내 도메인을 활용해 단축 URL을 만들고 싶지 않은가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안타깝게도 이 방법을 서술한 블로그는 전혀 보이지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 를 활용해 응용해보자. 필자는 다음과 같은 리디렉션 방식을 설계했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 사용자가 특정 서브도메인(a.klr.kr)에 대한 리디렉션(naver.com) 추가를 요청한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 서브도메인(a.klr.kr)은 네임서버(1.2.3.4)를 가리킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 네임서버의 80 포트는 Nginx이므로, Nginx에서 해당 리디렉션을 안내한다.&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;원하는 서비도메인은 Bing9에서 직접 네임서버를 가리키도록 추가하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 Nginx가 신경써야할 부분은 위 3번이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NginX 설치&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;conf 파일 수정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DNS 서버가 단독으로 공인 ip를 지닌 경우&lt;/h4&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;map $host $redirect_destination {
        hostnames;
        입력주소 http://연결대상주소;
        입력주소2 http://연결대상주소2;
                ...
}

server {
        listen 80;
        server_name _;

        location / {
                if ($redirect_destination) {
                        return 301 $redirect_destination$request_uri;
                }
        }
}&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;내 네임서버가 다른 VM(인스턴스)와 공인 ip를 공유하나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;= HyperVisor 환경에서 &quot;네임서버&quot;와 &quot;웹 서버&quot;를 격리된 인스턴스로 운용중이라면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 더보기&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DNS 서버가 다른 VM(인스턴스)와 공인 ip를 공유하는 경우&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;필자와 같이 KVM 등으로 DNS VM을 별도로 구축한 경우, &lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;Redirect 대상이 아닌 모든 요청은 웹서버 VM으로 다시 빠져야한다.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;안그러면 웹 서버에 접근이 안된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;map $host $redirect_destination {
        hostnames;
        입력주소 http://연결대상주소;
        입력주소2 http://연결대상주소2;
                ...
}

server {
        listen 80;
        server_name _;

        location / {
                if ($redirect_destination) {
                        return 301 $redirect_destination$request_uri;
                }

                proxy_pass http://웹서버VM주소;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 설정을 마치면 된다.&lt;span style=&quot;background-color: #0593d3; color: #ffffff;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;DNS 세팅 API 자동화 - Bind9 자동화&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;목표 : DNS 및 리디렉션을 자동화 하는 API 서버를 구축한다.&lt;/blockquote&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: 26.2791%;&quot;&gt;클라우드 서비스 (AWS EC2)&lt;/td&gt;
&lt;td style=&quot;width: 73.7209%;&quot;&gt;OnPremise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.2791%;&quot;&gt;우측 이미지에서&lt;br /&gt;녹색 영역만 보면 된다.&lt;/td&gt;
&lt;td style=&quot;width: 73.7209%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vQDLx/dJMcaaSfkiL/rhXNjYzmXuJ4USsTvVF1WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vQDLx/dJMcaaSfkiL/rhXNjYzmXuJ4USsTvVF1WK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vQDLx/dJMcaaSfkiL/rhXNjYzmXuJ4USsTvVF1WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvQDLx%2FdJMcaaSfkiL%2FrhXNjYzmXuJ4USsTvVF1WK%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;4400&quot; height=&quot;2508&quot; data-origin-width=&quot;4400&quot; data-origin-height=&quot;2508&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;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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Bind9 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bind의 zone 영역은 include가 불가능하다. 따라서 이를 스크립트화 시킬 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;change_record.sh&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;ZONES_DIR=&quot;/etc/bind/zones/list_dns&quot;
ZONE_FILE=&quot;/etc/bind/zones/zone파일명&quot;

cat &amp;gt; $ZONE_FILE &amp;lt;&amp;lt;EOF
;
; BIND data file for local loopback interface
;
\$TTL    300
@       IN      SOA     네임서버. 메일주소. (
                              2         ; Serial
                          86400         ; Refresh
                           7200         ; Retry
                        2419200         ; Expire
                          86400 )       ; Negative Cache TTL
; Name Server Info
@       3600    IN      NS      네임서버.
; Name Server A record
ns1     IN      A       네임서버ip
@       IN      A       네임서버ip

EOF

for file in $ZONES_DIR/*; do
        cat $file &amp;gt;&amp;gt; $ZONE_FILE
        #echo &quot;&quot; &amp;gt;&amp;gt; $ZONE_FILE
done&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;add_record.sh&lt;/h4&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;FILE_PATH=&quot;/etc/bind/zones/list_dns/$1&quot;

if [ -f &quot;$FILE_PATH&quot; ]; then
            rm &quot;$FILE_PATH&quot;
fi

echo &quot;$1        IN      $2      $3&quot; &amp;gt; &quot;$FILE_PATH&quot;

sudo /etc/bind/zones/change_record.sh

sudo rndc reload&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;del_record.sh&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;FILE_PATH=&quot;/etc/bind/zones/list_dns/$1&quot;

if [ -f &quot;$FILE_PATH&quot; ]; then
            rm &quot;$FILE_PATH&quot;
fi

sudo /etc/bind/zones/change_record.sh

sudo rndc reload&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DNS 레코드 한번에 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 /etc/bind/zones/에 list_dns 폴더를 제작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774430970097&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo mkdir list_reidrects&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/etc/bind/zones/list_dns/example1&amp;nbsp;의 내부 내용&lt;/p&gt;
&lt;pre id=&quot;code_1774430324273&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;example1        IN      A      1.2.3.4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/etc/bind/zones/list_dns/example2&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 내부 내용&lt;/p&gt;
&lt;pre id=&quot;code_1774430450484&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;example2        IN      A      5.6.7.8&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;앞으로 api를 요청 받으면 해당 폴더에&lt;br /&gt;[서브도메인]&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;[레코드명]&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;[대상주소]&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;를 담은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[서브도메인]&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 제작하기 위함이다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 api를 요청 받으면 서버는 다음과 같이 파일을 실행하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1774430947500&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/etc/bind/zones/add_record.sh [서브도메인] [레코드명] [대상주소]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;DNS 세팅 API 자동화 - Nginx 자동화&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화를 위해 Nginx의 리디렉션 경로들도 위와 같이 파일로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일로 관리하기 위해 Nginx의 conf 파일에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;입력주소 http://연결대상주소&quot; 형태의 목록을 include로 대체한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;/etc/nginx/conf.d/redirect.conf&lt;/h4&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;map $host $redirect_destination {
        hostnames;
        include /etc/nginx/conf.d/list_redirects/*; # 이 부분을 수정한다
}

server {
        listen 80;
        server_name _;

        location / {
                if ($redirect_destination) {
                        return 301 $redirect_destination$request_uri;
                }

                proxy_pass http://웹서버VM주소;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}&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;이후 /etc/nginx/conf.d/에&amp;nbsp;list_redirects&amp;nbsp;폴더를&amp;nbsp;제작한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774430963647&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo mkdir list_reidrects&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) /etc/nginx/conf.d/list_redirects/example1&amp;nbsp;의 내부 내용&lt;/p&gt;
&lt;pre id=&quot;code_1774430882914&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;example1.test.com https://naver.com&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이는 앞으로 api를 요청 받으면 해당 폴더에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;[요청&amp;nbsp;서브도메인&amp;nbsp;주소]&amp;nbsp;[리다이렉트&amp;nbsp;주소]&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 를 담은 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;[서브도메인]&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;파일을 제작하기 위함이다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;add_redirects.sh&lt;/h4&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;DOMAIN_NAME=&quot;.도메인주소&quot;
FILE_PATH=&quot;/etc/nginx/conf.d/list_redirects/$1&quot;

if [ -f &quot;$FILE_PATH&quot; ]; then
            rm &quot;$FILE_PATH&quot;
fi

echo &quot;$1$DOMAIN_NAME $2&quot; &amp;gt; &quot;$FILE_PATH&quot;

sudo /etc/bind/zones/add_record.sh $1 A 네임서버ip

sudo nginx -s reload&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;del_redirects.sh&lt;/h4&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;FILE_PATH=&quot;/etc/nginx/conf.d/list_redirects/$1&quot;

if [ -f &quot;$FILE_PATH&quot; ]; then
            rm &quot;$FILE_PATH&quot;
fi

sudo /etc/bind/zones/del_record.sh $1

sudo nginx -s 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;이제 api를 요청 받으면 다음과 같이 실행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1774431012397&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/etc/nginx/conf.d/add_redirects.sh [서브도메인] [대상주소]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;DNS 세팅 API 자동화 - API를 통해 DNS, Nginx 관리 자동화&lt;/b&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;목표 : API를 통해 &quot;서브도메인과 레코드&quot; 또는 &quot;리디렉션 주소&quot;를 정하면, 해당 세팅을 자동화하는 서버 제작&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 페이지에서는 DNS 서버의 SpringBoot영역을 구성하고 마무리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DTO&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 받을 정보들을 담을 DTO를 먼저 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Application을 DB 저장을 하지 않고 process만 처리하므로, Entity는 생성하지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DNSRecordDTO&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Data
public class DNSRecordDTO {
    // 양식
    // subdomain    A   1.2.3.4
    // subdomain    CNAME   example.com. (뒤에 온점)
    private String subdomain;
    private String record;
    private String targetAddress;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RedirectDTO&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Data
public class RedirectDTO {
    private String subdomain;
    private String targetAddress;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DeleteDomainDTO&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Data
public class DeleteDomainDTO {
    private String subdomain;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO를&amp;nbsp;기반으로&amp;nbsp;&amp;lsquo;Record,&amp;nbsp;Redirection&amp;nbsp;생성&amp;nbsp;및&amp;nbsp;삭제&amp;rsquo;을&amp;nbsp;위해&amp;nbsp;생성해두었던&amp;nbsp;~.sh를&amp;nbsp;실행시킨다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Service
public class DnsService {

    private void executeScript(String scriptPath, String... args){
        try {
            List&amp;lt;String&amp;gt; command = new ArrayList&amp;lt;&amp;gt;();
            command.add(&quot;sudo&quot;);
            command.add(scriptPath);
            Collections.addAll(command, args);

            ProcessBuilder processBuilder = new ProcessBuilder(command);
            Process process = processBuilder.start();
            process.waitFor();
        } catch (Exception e) {
            throw new NotRunnableException(scriptPath);
        }
    }
    public void addDNSRecord(DNSRecordDTO dnsRecordDTO){
        String targetAddress = dnsRecordDTO.getTargetAddress();
        if(dnsRecordDTO.getRecord().equals(&quot;CNAME&quot;)){
            targetAddress = targetAddress+ &quot;.&quot;;
        }
        executeScript(&quot;/etc/bind/zones/add_record.sh&quot;, dnsRecordDTO.getSubdomain(), dnsRecordDTO.getRecord(), targetAddress);
    }

    public void addRedirect(RedirectDTO redirectDTO){
        executeScript(&quot;/etc/nginx/conf.d/add_redirects.sh&quot;, redirectDTO.getSubdomain(), redirectDTO.getTargetAddress());
    }

    public void delDomain(String subDomain){
        executeScript(&quot;/etc/nginx/conf.d/del_redirects.sh&quot;, subDomain);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Exception&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서 생성한 Exception을 정의한다&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;public class NotRunnableException extends RuntimeException{
    public NotRunnableException(String filepath){
        super(&quot;파일 실행에 실패함: &quot;+filepath);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일반 Controller&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service를 바탕으로, 이를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/v1&quot;)
public class DnsController {

    private final DnsService dnsService;

    @PostMapping(&quot;/records&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; addDNSRecord(@RequestBody DNSRecordDTO dnsRecordDTO){
        dnsService.addDNSRecord(dnsRecordDTO);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

    @PostMapping(&quot;/redirects&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; addRedirects(@RequestBody RedirectDTO redirectDTO){
        dnsService.addRedirect(redirectDTO);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

    @DeleteMapping(&quot;/subdomain/{subdomain}&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; delDomain(@PathVariable String subdomain){
        dnsService.delDomain(subdomain);
        return ResponseEntity.status(HttpStatus.OK).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;예외 처리 Controller&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생시킨 Exception에 대한 Response를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@ControllerAdvice
public class ExceptionHandlerControllerAdvice {

    @ExceptionHandler({NotRunnableException.class})
    public ResponseEntity&amp;lt;?&amp;gt; handleNotRunnableException(NotRunnableException e){
        System.out.println(e.getMessage());
        return new ResponseEntity&amp;lt;&amp;gt;(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1 style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주변에서도 유사한 서비스를 구현하고 싶은데, 방법을 몰라 구축을 못한 사례들이 종종 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글도 없고, AI도 해결을 못해서 어려운게 아닐까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 서비스 구축을 희망하는 이들에게 도움이 되길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;참고 문헌&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/love_tolty/222690840923&quot;&gt;https://m.blog.naver.com/love_tolty/222690840923&lt;/a&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://dev.dwer.kr/2020/04/bind-9.html&quot;&gt;https://dev.dwer.kr/2020/04/bind-9.html&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 DNS 갱신시간 관련 Webtizen 설명&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.webtizen.co.kr/support/faq/cs_faq_view?uid=2&amp;amp;vid=27&amp;amp;rm=50&amp;amp;this_page=6&quot;&gt;https://www.webtizen.co.kr/support/faq/cs_faq_view?uid=2&amp;amp;vid=27&amp;amp;rm=50&amp;amp;this_page=6&lt;/a&gt;&lt;/p&gt;</description>
      <category>백엔드/자유</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/20</guid>
      <comments>https://leestana01.tistory.com/20#entry20comment</comments>
      <pubDate>Wed, 25 Mar 2026 18:35:09 +0900</pubDate>
    </item>
    <item>
      <title>내부망 구성하기 (개정판)</title>
      <link>https://leestana01.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발서버 분리를 위해 VPN 환경에서만 접근 가능한 내부 망을 구성한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 너무 엄격하게 한 나머지, DNS 쿼리를 해도 내부망에 접근 못하게 구성했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VPN으로 접근된 트래픽을 Squid 를 통해 Proxy를 구성하고, HTTPS를 MITM시켜 구성했다.&lt;br /&gt;그런데 네이버, 구름 등 대기업들도 이렇게까진 하지 않더라...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 정리한 게시글(&lt;a href=&quot;https://leestana01.tistory.com/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leestana01.tistory.com/6&lt;/a&gt;)이 있는데, 너무 과거 회고에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 내부망 구성 방식을 보기좋게 다시 고안하고 정리한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;설계 목표&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp;인가되지&amp;nbsp;않은&amp;nbsp;외부인의&amp;nbsp;개발&amp;nbsp;리소스&amp;nbsp;접근을&amp;nbsp;일체&amp;nbsp;금지한다.&lt;br /&gt;-&amp;nbsp;내부&amp;nbsp;리소스의&amp;nbsp;모든&amp;nbsp;접근은&amp;nbsp;VPN을&amp;nbsp;이용해&amp;nbsp;접근해야한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;하단에 기술할 구성 방법은 본인이 직접 구상하여 설계한 것으로,&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;더 좋은 실무적 방법이 있는 경우 공유해주시면 감사할 듯 하다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;방법 1 - Ingress에서 차단하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로 이 방식을 사용중이다. 매우 간단한데, 매우 확실하다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적인 우려가 있다면 굳이 ingress까지 트래픽이 한 번 타고 가야한다는 점이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #0593d3; color: #ffffff;&quot;&gt;&amp;nbsp; externalTrafficPolicy&amp;nbsp;변경&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 사용자가 VPN 환경에서 접속했는지 Client-IP를 확인해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 기존 `externalTrafficPolicy: Cluster`에서는 ingress-nginx가 실제 클라이언트 IP를 볼 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 모든 트래픽이 kube-proxy를 거쳐 노드 IP로 변환되기 때문&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;조금 더 상세히 설명하면 다음과 같이 SNAT이 발생하여, Client-IP가 유실된다.&lt;br /&gt;Client &amp;rarr; Node A &amp;rarr; (kube-proxy) &amp;rarr; Node B의 Pod&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;color: #ef5369;&quot;&gt;&lt;b&gt;Local로 변경&lt;/b&gt;&lt;/span&gt; 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(주의 : kubectl 직접 패치이므로 ingress-nginx Helm 재설치 시 재적용 필요!)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;kubectl patch svc ingress-nginx-controller -n ingress-nginx \&lt;br /&gt;&amp;nbsp; -p '{&quot;spec&quot;:{&quot;externalTrafficPolicy&quot;:&quot;Local&quot;}}'&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #0593d3; color: #ffffff;&quot;&gt;&amp;nbsp; 환경 분석&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;VPN 환경이 다음과 같다고 가정하자.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;-&amp;nbsp;VPN: WireGuard (wg-easy)&lt;br /&gt;-&amp;nbsp;클라이언트 서브넷:&amp;nbsp;`10.8.0.0/24`&lt;br /&gt;-&amp;nbsp;VPN pod IP:&amp;nbsp;`10.0.10.51`&amp;nbsp;(node 10.0.10.220)&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트래픽 경로는 다음과 같을 것이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;externalTrafficPolicy: Cluster 라면 어떻게 되나요?&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;VPN 클라이언트 (10.8.0.x)&lt;br /&gt;&amp;nbsp; &amp;rarr; WireGuard 터널&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;rarr; VPN pod (MASQUERADE &amp;rarr; 10.0.*.*)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; NAT Gateway (SNAT &amp;rarr; 152.69.*.*)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; OCI LB (168.107.*.*)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; kube-proxy (SNAT &amp;rarr; 노드 IP)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; ingress-nginx (sees: 노드 IP)&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;VPN 클라이언트 (10.8.0.x)&lt;br /&gt;&amp;nbsp; &amp;rarr; WireGuard 터널&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;rarr; VPN pod (MASQUERADE &amp;rarr; 10.0.*.*)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; 목적지: dev.notinoty.kr = 168.107.*.* (OCI LB)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; OCI 내부 라우팅 (같은 VCN이므로 NAT Gateway 미경유, SNAT 없음)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; OCI LB (168.107.*.*)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;rarr; ingress-nginx (sees: 10.0.*.*) &amp;larr; VPN Pod IP 그대로 보존&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;즉, VPN 트래픽의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;source IP =&amp;nbsp; VPN pod (10.0.*.*)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #0593d3; color: #ffffff;&quot;&gt;&amp;nbsp; Ingress 수정&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 각 프로젝트의 gitops(kustomization.yaml)의 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;기존 patches에 overlay로 다음을 추가&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(혹시 몰라 SNAT을 고려하여 NAT 주소까지 추가했다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;patches:&lt;br /&gt;&amp;nbsp; # 기존 patches에 추가&lt;br /&gt;&amp;nbsp; - patch: |-&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; - op: add&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; path: /metadata/annotations/nginx.ingress.kubernetes.io~1whitelist-source-range&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; value: &quot;10.0.*.*/32,152.69.*.*/32&quot;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; target:&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; kind: Ingress&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gitops가 아닌 자체 배포를 사용중이라면, ingress에 annotaion을 직접 추가하면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;apiVersion: networking.k8s.io/v1&lt;br /&gt;kind: Ingress&lt;br /&gt;metadata:&lt;br /&gt;&amp;nbsp; name: ingress이름&lt;br /&gt;&amp;nbsp; annotations:&lt;br /&gt;&amp;nbsp; &amp;nbsp; nginx.ingress.kubernetes.io/whitelist-source-range: &quot;10.0.*.*/32,152.69.*.*/32&quot;&lt;br /&gt;spec:&lt;br /&gt;...(생략)...&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;방법 2 - DNS에서 차단하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;역시나 매우 간단하다. &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;k8s의 해당 Service주소를 DNS로&lt;/b&gt;&lt;/span&gt; 가리키면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, DNS-01 방식을 통해 인증해야한다는 점이 치명적인 단점이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DNS-01은 txt 기반으로 인증&lt;/b&gt;된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;DNS Provider API가 없다면 인증서를 매번 수동 갱신&lt;/b&gt;&lt;/span&gt;해야한다. (네임서버에서 txt 직접 수정해야함)&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DNS Provider API는 보통 다음을 지원한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cloudflare&lt;/li&gt;
&lt;li&gt;Route53&lt;/li&gt;
&lt;li&gt;Google DNS&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자는 호스팅케이알을 이용중이므로, 적합하지 않다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원되는 환경이라면 와일드카드 지원을 포함하여 꽤 유용하다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 트래픽을 DNS 수준에서 걸러버릴 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;간단해서 여기에 방법은 기재하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;방법 3 - 외부에서 존재조차 모르게 차단하기&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;방법이 너무 복잡하므로, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;아래 이미지&lt;/b&gt;&lt;/span&gt;로 갈음한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;과거에 직접 구성했던 VPN 환경을 이미지로 표현한 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사실상 필요한 내용이 모두 담겨있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, Squid 설정이 인터넷에도 없고 AI도 제대로 구성 못한다. 부딪히면서 공부하는게 답이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;721&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxrZZc/dJMcahKyN18/gj81QsVnA0hVEvxXVKWvN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxrZZc/dJMcahKyN18/gj81QsVnA0hVEvxXVKWvN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxrZZc/dJMcahKyN18/gj81QsVnA0hVEvxXVKWvN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxrZZc%2FdJMcahKyN18%2Fgj81QsVnA0hVEvxXVKWvN1%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;629&quot; height=&quot;721&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;721&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인프라/자유</category>
      <author>leestana01</author>
      <guid isPermaLink="true">https://leestana01.tistory.com/19</guid>
      <comments>https://leestana01.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 23 Mar 2026 18:14:48 +0900</pubDate>
    </item>
  </channel>
</rss>