<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>dev.j</title>
    <link>https://jhost.tistory.com/</link>
    <description>ddongjunn@gmail.com</description>
    <language>ko</language>
    <pubDate>Sun, 12 Apr 2026 23:56:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>:j</managingEditor>
    <image>
      <title>dev.j</title>
      <url>https://tistory1.daumcdn.net/tistory/3884792/attach/f23295b048fd410da75055a694725c87</url>
      <link>https://jhost.tistory.com</link>
    </image>
    <item>
      <title>Ceph 장애가 발생했을 때, 왜 BE가 느려졌을까?</title>
      <link>https://jhost.tistory.com/113</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ceph 클러스터 장애가 발생했을 때,&lt;/span&gt;&lt;span&gt;BE가 계속 retry를 하면서 오히려 응답이 더 느려지고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Ceph API를 활용해서 Ceph 데이터를 서빙하는 백엔드를 개발하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;FE &amp;rarr; G/W &amp;rarr; BE &amp;rarr; Ceph MGR API (Active-Standby 구조)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 흐름 자체는 단순한데, Ceph의 Active-Standby 구조 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;303 Redirect, Fallback, Retry, Active MGR 갱신 같은 걸 전부 BE에서 직접 처리하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에는 별 문제 없이 돌아갔는데, Ceph &lt;span&gt;장애 상황에서는 다음과 같은 문제가 발생했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답 시간이 1.15초까지 증가&lt;/li&gt;
&lt;li&gt;redirect &amp;rarr; retry &amp;rarr; host 순회 &amp;rarr; 재귀 호출 구조&lt;/li&gt;
&lt;li&gt;장애 원인 파악이 어려움(인프라 문제인지, Ceph 문제인지, BE 문제인지)&lt;/li&gt;
&lt;li&gt;코드 복잡도 증가 및 유지보수 비용 상승&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;성능 최적화 문제겠지&quot; 하고 단순하게 생각했습니다. 재시도 로직을 좀 더 정교하게 만들면 되지 않을까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 장애 상황을 재현해보면서 생각이 바뀌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 본질은 성능이나 재시도 로직이 아니라, &lt;b&gt;&quot;장애 처리 책임이 잘못된 레이어에 있었다&quot;&lt;/b&gt;는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Active MGR 선택, Failover, Health Check &amp;mdash; 이건 인프라가 해야 하는 일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때부터 이 문제를 단순 리팩토링이 아닌 &lt;b&gt;구조적 재설계 문제&lt;/b&gt;로 다시 정의했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: 측정 가능한 상태 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예상치 못한 발견&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 개선하기 전에, 먼저 기존 구조가 장애 상황에서 실제로 어떻게 동작하는지 측정해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k6로 부하 테스트를 구성해서 두 가지를 비교했습니다. (Ceph 클러스터 장애 상황 기준)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 로직 (redirect &amp;rarr; retry &amp;rarr; host 순회)&lt;/li&gt;
&lt;li&gt;redirect 및 fallback 제거 후 단순 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 테스트를 돌리자 예상과 전혀 다른 결과가 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장애를 측정하려 했는데 측정 자체가 불가능한 상태였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 분석해보니 Thundering Herd 문제가 숨어있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 Provider에 동시 요청 &amp;rarr; 인증 토큰 발급 API가 요청 수만큼 중복 호출&lt;/li&gt;
&lt;li&gt;토큰 캐시 덮어쓰기&lt;/li&gt;
&lt;li&gt;Active MGR 갱신 이벤트가 중복 발행 &amp;rarr; DB Optimistic Lock 충돌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE 내부가 안정적이지 않으면 인프라 구조 개선의 효과를 검증할 수 없는 상태였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Single-flight 패턴 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 측정 가능한 상태를 만드는 게 첫 번째라고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;synchronized&lt;/code&gt;나 &lt;code&gt;ReentrantLock&lt;/code&gt;으로 토큰 발급 구간을 순차 실행하는 방식을 검토했습니다.&lt;br /&gt;근데 락 내부에서 외부 HTTP 호출이 수행되는 구조다 보니, 응답이 지연되면 모든 요청이 임계 구역 앞에서 줄을 서게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 제어가 아니라 병목을 만드는 방식이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Redis 기반 분산 락을 검토했습니다. 멀티 인스턴스 환경이니까 당연히 떠오르는 선택지였는데, 설계를 구체화하다 보니 고민이 생겼습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 추가 네트워크 왕복&lt;/li&gt;
&lt;li&gt;락 타임아웃 및 실패 처리 로직&lt;/li&gt;
&lt;li&gt;락 보유 인스턴스 장애 시 복구 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하려고 또 하나의 장애 지점을 추가하는 꼴이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 한 가지 기준이 생겼습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 토큰 발급 과정에서 정말 모든 인스턴스 사이에 완벽한 상호배제가 필요할까?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영 환경에는 약 3개의 인스턴스가 돌고 있습니다.&lt;br /&gt;분산 락 없이 최악의 상황에서도 동시 중복 호출은 최대 3회입니다.&lt;br /&gt;토큰은 Redis에 TTL 15분으로 캐시되고 대부분의 요청은 캐시 히트로 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;문제가 되는 구간은 &lt;b&gt;토큰 만료 직후의 매우 짧은 순간&lt;/b&gt;뿐이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ceph 인증 API는 JWT 기반의 stateless 구조라서, 반복 호출해도 클러스터 상태를 변경하지 않습니다.&lt;br /&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;&quot;복잡하게 해결하기보다, 지금 시스템에 맞는 수준에서 단순하게 해결&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락으로 시스템 복잡도를 올리는 것보다, 인스턴스 내부에서만 동시 요청 제어만으로 충분하다고 생각해서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Single-flight 패턴 &lt;/b&gt;을 적용했습니다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;getOrIssueToken()
├─ Redis 캐시 조회 (Cache Hit 시 즉시 반환)
├─ cache miss
│
├─ inFlight.computeIfAbsent(providerId)
│ └─ VirtualThreadExecutor.supplyAsync()
│   └─ issuer.call() &amp;larr; Ceph /api/auth
│
├─ 다른 요청 &amp;rarr; future.join() (가상 스레드 파킹 후 결과만 공유)
└─ 결과 반환&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;완벽한 상호배제는 아니지만, 실질적 중복 호출은 대부분 제거 가능하고 추가 인프라 의존성도 없습니다.&lt;/p&gt;
&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;토큰 발급 API 호출 중복 제거 (N &amp;rarr; 1 수준으로 수렴)&lt;/li&gt;
&lt;li&gt;DB Lock 경합 해소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰할 수 있는 테스트 환경 확보&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Single-flight를 먼저 도입해서 &quot;측정 가능한 상태&quot;를 만든 것이 이후 모든 개선의 시작점이 되었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: 인프라 책임 분리 (HAProxy + VIP)&lt;/h2&gt;
&lt;!-- 아키텍처 다이어그램 (도입 전) --&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BE 내부를 안정화한 뒤, 이제 진짜 궁금했던 걸 측정할 수 있게 됐습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br4bWn/dJMcacP7pTV/OxgfPzUu97jxR3Uyfxk2jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br4bWn/dJMcacP7pTV/OxgfPzUu97jxR3Uyfxk2jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br4bWn/dJMcacP7pTV/OxgfPzUu97jxR3Uyfxk2jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr4bWn%2FdJMcacP7pTV%2FOxgfPzUu97jxR3Uyfxk2jK%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;660&quot; height=&quot;393&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 장애 상황(Ceph Cluster Down)에서 비교해봤습니다.&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;기존 로직 (redirect &amp;rarr; retry &amp;rarr; host 순회)&lt;/td&gt;
&lt;td&gt;1,150ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redirect 및 fallback 제거 후 단순 호출&lt;/td&gt;
&lt;td&gt;7.2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1,150ms &amp;rarr; 7.2ms. 160배 차이.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수치를 보고 확신이 들었습니다. 문제는 &quot;재시도를 어떻게 잘 할 것인가&quot;가 아니라, &lt;b&gt;&quot;이 로직들이 애초에 BE의 책임인가&quot;&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 BE는 Active MGR 선택, StandBy 판단, 장애 감지와 Fallback까지 전부 직접 하고 있었습니다.&lt;br /&gt;장애가 발생하면 BE 내부 로직이 복잡하게 얽히면서 원인 분리도 안 되고, 레이턴시만 올라갔습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;장애를 더 잘 처리하는 코드보다, 장애가 BE로 전파되지 않도록 경계를 명확히 하는 게 더 중요하다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 BE가 더이상 MGR 선택과 장애 판단을 직접 하지 않도록, Ceph Cluster 앞에 Reverse Proxy를 두고 BE는 항상 단일 진입점(VIP)만 호출하는 구조로 바꿨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reverse Proxy로 Nginx도 가능했지만, &lt;b&gt;HAProxy + Keepalived&lt;/b&gt;를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ceph 공식 문서가 MGR HA 구성의 표준으로 제시하는 조합이었고, 검증된 아키텍처를 따르는 게 장애 대응과 협업 측면에서 리스크가 낮다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HAProxy 책임:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Active MGR 선택 (Health Check 기반)&lt;/li&gt;
&lt;li&gt;StandBy / Down MGR 자동 제외&lt;/li&gt;
&lt;li&gt;BE는 VIP만 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BE에서 제거된 로직:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;303 Redirect 파싱&lt;/li&gt;
&lt;li&gt;Host 순차 Fallback&lt;/li&gt;
&lt;li&gt;재귀적 재시도&lt;/li&gt;
&lt;li&gt;Active MGR 갱신 이벤트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BE에 남긴 최소 책임:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;401 Unauthorized &amp;rarr; Single-flight 기반 토큰 갱신 1회 재시도&lt;/li&gt;
&lt;li&gt;그 외 timeout / 5xx &amp;rarr; Fast-Fail&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가용성 판단 &amp;rarr; 인프라 책임&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증 관리 &amp;rarr; 애플리케이션 책임&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 더 단순해졌고, 장애 원인은 명확히 분리됐습니다. 그리고 가장 중요한 건 &lt;b&gt;시스템이 예측 가능해졌다&lt;/b&gt;는 점입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRuUj0/dJMcafe2OsQ/xldqyFdD226Mkygl8Nvk2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRuUj0/dJMcafe2OsQ/xldqyFdD226Mkygl8Nvk2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRuUj0/dJMcafe2OsQ/xldqyFdD226Mkygl8Nvk2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRuUj0%2FdJMcafe2OsQ%2FxldqyFdD226Mkygl8Nvk2K%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;660&quot; height=&quot;369&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- 아키텍처 다이어그램 (도입 후) --&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: 애플리케이션 회복력 (Resilience4j)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HAProxy 도입으로 MGR 선택과 장애 차단은 인프라로 넘겼습니다. BE는 이제 &quot;어디로 요청을 보낼지&quot;를 고민하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 여전히 남은 문제가 있었습니다. &lt;b&gt;&quot;실패를 애플리케이션이 어떤 규칙으로 다룰 것인가&quot;&lt;/b&gt;에 대한 정책이 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 오류는 재시도해야 하고, 어떤 오류는 즉시 실패해야 하고, 어떤 장애는 공급자 단위로 격리되어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라가 '빠른 실패'를 가능하게 만들었으니, 이제 애플리케이션은 실패에 어떻게 반응할지를 명시적으로 가져야 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Resilience4j인가&lt;/h3&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;Spring Retry&lt;/td&gt;
&lt;td&gt;CB/Bulkhead 미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hystrix&lt;/td&gt;
&lt;td&gt;2018년 deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Resilience4j&lt;/b&gt;&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;Spring Boot 3 환경에서 공식 지원되고, 설정 외부화와 메트릭 연동이 용이해서 선택했습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Bulkhead&lt;/b&gt;: CB OPEN 시 FE가 이미 차단 &amp;rarr; 스레드 고갈 위험 낮음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RateLimiter&lt;/b&gt;: 내부망 환경으로 트래픽이 낮으며, Ceph에는 Rate Limit 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TimeLimiter&lt;/b&gt;: 동기 호출이라 RestClient 타임아웃으로 충분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 필요한 건 &lt;b&gt;Retry + CircuitBreaker&lt;/b&gt; 두 가지였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설계&lt;/h3&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;실행 순서: CB( Retry( Supplier ) )
- Retry 먼저: 일시적 오류(502, 503) 복구 시도
- CB 나중: Retry까지 실패한 &quot;최종 결과&quot;만 실패율 반영
- 반대면? Retry 시도마다 CB 카운트 &amp;rarr; 성급한 OPEN&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;resilience4j:
  retry:
    retryExceptions:
      - BadGateway          # 502: HAProxy 일시 오류 (재시도 가능)
      - ServiceUnavailable  # 503: MGR 준비 안 됨 (재시도 가능)
    ignoreExceptions:
      - InternalServerError # 500: Ceph 로직 오류 (재시도 무의미)

  circuitbreaker:
    ignoreExceptions:
      - InvalidCredentialsException  # 인증 오류 &amp;ne; 시스템 장애&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Provider별 격리&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String key = &quot;ceph-&quot; + providerUuid;  // 클러스터마다 독립 CB

Supplier&amp;lt;T&amp;gt; decoratedSupplier = Retry.decorateSupplier(retry, supplier);
decoratedSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, decoratedSupplier);
return decoratedSupplier.get();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cluster A 장애 &amp;rarr; A의 CB만 OPEN &amp;rarr; B, C 정상 동작.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j를 도입하면서 한 가지 의문이 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;application.yml의 설정값들이 실제 장애 상황에서 100% 의도대로 동작할까?&quot;&lt;/p&gt;
&lt;/blockquote&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;less&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;InvalidCredentialsException 은 CircuitBreaker 실패로 기록되지 않는다&quot;)
void circuitBreakerIgnoreInvalidCredentialsException() {
    for (int i = 0; i &amp;lt; 10; i++) {
        assertThrows(InvalidCredentialsException.class, ...);
    }
    // CB는 여전히 CLOSED (의도한 동작)
    assertThat(cb.getState()).isEqualTo(CLOSED);
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- 테스트 통과 스크린샷 삽입 --&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvT650/dJMcaa5Qi3l/AAAzh2UAXHfAwZcLz6c5o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvT650/dJMcaa5Qi3l/AAAzh2UAXHfAwZcLz6c5o0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvT650/dJMcaa5Qi3l/AAAzh2UAXHfAwZcLz6c5o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvT650%2FdJMcaa5Qi3l%2FAAAzh2UAXHfAwZcLz6c5o0%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;660&quot; height=&quot;411&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조 변경만으로 장애 상황에서 다음과 같은 개선을 얻을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DjTtj/dJMcaaEM4m1/66Ok0XxummyzOUmU7J4Xzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DjTtj/dJMcaaEM4m1/66Ok0XxummyzOUmU7J4Xzk/img.png&quot; data-alt=&quot;Before&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DjTtj/dJMcaaEM4m1/66Ok0XxummyzOUmU7J4Xzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDjTtj%2FdJMcaaEM4m1%2F66Ok0XxummyzOUmU7J4Xzk%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;660&quot; height=&quot;321&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Before&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXRwAq/dJMcacCA6Ae/pfK0Ycx1rXQyK8HAVttSOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXRwAq/dJMcacCA6Ae/pfK0Ycx1rXQyK8HAVttSOk/img.png&quot; data-alt=&quot;After&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXRwAq/dJMcacCA6Ae/pfK0Ycx1rXQyK8HAVttSOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXRwAq%2FdJMcacCA6Ae%2FpfK0Ycx1rXQyK8HAVttSOk%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;660&quot; height=&quot;361&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;After&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- k6 before/after 스크린샷 삽입 --&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;th&gt;개선 후&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;평균 응답시간&lt;/td&gt;
&lt;td&gt;5.04s&lt;/td&gt;
&lt;td&gt;36.5ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P95 레이턴시&lt;/td&gt;
&lt;td&gt;10.03s&lt;/td&gt;
&lt;td&gt;78ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dropped_iterations&lt;/td&gt;
&lt;td&gt;989&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 VUs&lt;/td&gt;
&lt;td&gt;50 (max 도달)&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 클러스터 fast-fail&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;98~99% (100ms 이내)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CB 상태 모니터링&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;td&gt;Actuator 실시간 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수치는 정상 상황의 성능 향상이 아니라, &lt;b&gt;장애 상황에서의 회복 탄력성 차이&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 여정 요약&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;장애를 어떻게 처리할지가 아니라, 누가, 어느 레이어에서 책임질 것인가&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1단계: 측정 가능한 상태 만들기 (Single-flight)
├─ BE 내부 동시성 문제 제거
├─ 토큰 재발급 Thundering Herd 해소
└─ 성과: 신뢰 가능한 장애 테스트 환경 확보

2단계: 인프라 책임 분리 (HAProxy + VIP)
├─ MGR 선택 / Failover &amp;rarr; 인프라로 위임
├─ Fast-Fail &amp;rarr; 장애 전파 차단
└─ 성과: 평균 응답 160배 개선 (1,150ms &amp;rarr; 7.2ms)

3단계: 애플리케이션 회복력 (Resilience4j)
├─ Provider별 CircuitBreaker &amp;rarr; 클러스터 격리
├─ 정밀 Retry 정책 &amp;rarr; 불필요한 재시도 제거
└─ 성과: 장애 격리 + 실시간 관찰 가능성 확보&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 정의의 재발견&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 팀내 FE 개발자분이 &quot;API 응답 속도가 너무 느리다&quot;고 알려준 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 Ceph 클러스터 장애로 실제 API 통신이 불가능한 상황이었는데, BE는 매 요청마다 Ceph로 다시 시도하며 에러를 반환하고 있었습니다. 장애가 발생한 클러스터로의 요청이 계속 시스템 내부로 유입되면서 레이턴시와 리소스 점유가 누적되고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때까지 저는 이 문제를 &quot;이 로직들을 어떻게 개선할까?&quot;라는 How의 문제로만 보고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 구조를 뜯어볼수록 의문이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;왜 이걸 BE에서 다 처리하고 있지?&quot;, &quot;이 책임들은 인프라가 가져야 하는 거 아닌가?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 순간 문제가 다르게 보였습니다. 단순한 성능 최적화가 아니라 &lt;b&gt;책임을 다시 배치해야 하는 설계 문제&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복잡한 설정은 검증되지 않은 코드와 같다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j 설정이 의도대로 동작하는지를 테스트 코드로 증명하면서 다시 한번 느꼈습니다.&lt;br /&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;b&gt;장애가 전파되지 않을 구조를 설계하는 것&lt;/b&gt;이 더 중요하다는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Active MGR 선택, Failover, 장애 감지는 인프라가 더 안정적으로 수행할 수 있는 영역이었고, BE는 정상 요청 처리와 인증 관리에 집중하는 편이 시스템 전체의 예측 가능성과 안정성에 훨씬 유리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 중요한 건 기술을 얼마나 많이 아느냐가 아니라, &lt;/span&gt;&lt;b&gt;&quot;각 레이어에 맞는 책임을 어디에 둘 것인가&quot;를 판단하는 능력&lt;/b&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;div&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>dev/Ceph</category>
      <category>ceph</category>
      <category>HAProxy</category>
      <category>Resilience4J</category>
      <category>백엔드</category>
      <category>시스템설계</category>
      <category>아키텍처</category>
      <category>장애대응</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/113</guid>
      <comments>https://jhost.tistory.com/113#entry113comment</comments>
      <pubDate>Tue, 31 Mar 2026 20:49:40 +0900</pubDate>
    </item>
    <item>
      <title>[Ceph] Custom Container 배포를 해보자! (+Prometheus 연동)</title>
      <link>https://jhost.tistory.com/112</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceLYsH/dJMcac9k8C3/Enr1zwuowLEntMyQKKmmI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceLYsH/dJMcac9k8C3/Enr1zwuowLEntMyQKKmmI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceLYsH/dJMcac9k8C3/Enr1zwuowLEntMyQKKmmI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceLYsH%2FdJMcac9k8C3%2FEnr1zwuowLEntMyQKKmmI0%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;1390&quot; height=&quot;638&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RBD 이미지 -&amp;gt; PG -&amp;gt; OSD 매핑&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ceph&amp;nbsp;클러스터를&amp;nbsp;운영하다&amp;nbsp;보면&amp;nbsp;장애&amp;nbsp;분석이나&amp;nbsp;성능&amp;middot;용량&amp;nbsp;이슈가&amp;nbsp;생겼을&amp;nbsp;때,&amp;ldquo;지금&amp;nbsp;보고&amp;nbsp;있는&amp;nbsp;RBD&amp;nbsp;이미지가&amp;nbsp;실제로&amp;nbsp;어떤&amp;nbsp;PG에&amp;nbsp;올라가&amp;nbsp;있고,&amp;nbsp;그&amp;nbsp;PG가&amp;nbsp;어떤&amp;nbsp;OSD&amp;nbsp;들에&amp;nbsp;퍼져&amp;nbsp;있는지&amp;rdquo;를&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;1. 특정 OSD 장애가 발생했을 때, 그 OSD에 어떤 이미지가 얼마나 몰려 있는지 보고 싶거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 리밸런싱 이후 데이터가 의도대로 고르게 분산됐는지 확인하고 싶은경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Ceph 내부에서는 객체 이름과 풀 ID로 해시해서 PG를 계산하고 이 PG를 다시 CRUSH로 여러 OSD에 배치를 하게 됩니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 go-cpeh(librados를 go 언어로 래핑한 오픈소스)를 사용하여 RBD 이미지 -&amp;gt; PG -&amp;gt; OSD 매핑 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;이후 해당 관련 내용은 다음 포스팅에서 다루도록 하겠습니다!&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Ceph MGR Module vs Custom Container&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;219&quot; data-annotation-mark=&quot;true&quot; data-annotation-inline-node=&quot;true&quot; data-card-url=&quot;https://docs.ceph.com/en/latest/mgr/modules/&quot; data-inline-card=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;219&quot; data-annotation-mark=&quot;true&quot; data-annotation-inline-node=&quot;true&quot; data-card-url=&quot;https://docs.ceph.com/en/latest/mgr/modules/&quot; data-inline-card=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;span data-testid=&quot;inline-card-icon-and-title&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ceph&amp;nbsp;MGR&amp;nbsp;Module&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MGR&amp;nbsp;Module:&amp;nbsp;&lt;a href=&quot;https://docs.ceph.com/en/latest/mgr/modules/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.ceph.com/en/latest/mgr/modules/&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-inline-card=&quot;true&quot; data-card-url=&quot;https://docs.ceph.com/en/reef/cephadm/services/custom-container/&quot; data-annotation-inline-node=&quot;true&quot; data-annotation-mark=&quot;true&quot; data-renderer-start-pos=&quot;507&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;span data-testid=&quot;inline-card-icon-and-title&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Ceph-Mgr 데몬 내부에 동작하는 Python 기반의 플러그인 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Python으로 MgrModule 클래스를 상속 받아서 구현해야 하며, serve(), notify(), handler_command()등의 메소드를 오버라이드 구현이 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Ceph 클러스터 내부 데이터(모니터, OSD, PG 등) 밀접하게 연동하여 기능 확장이 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ceph mgr module enable &amp;lt;my_module&amp;gt; 로 활성화 가능&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;507&quot; data-annotation-mark=&quot;true&quot; data-annotation-inline-node=&quot;true&quot; data-card-url=&quot;https://docs.ceph.com/en/reef/cephadm/services/custom-container/&quot; data-inline-card=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;span data-testid=&quot;inline-card-icon-and-title&quot;&gt;&lt;span&gt;Custom Container&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-inline-card=&quot;true&quot; data-card-url=&quot;https://docs.ceph.com/en/reef/cephadm/services/custom-container/&quot; data-annotation-inline-node=&quot;true&quot; data-annotation-mark=&quot;true&quot; data-renderer-start-pos=&quot;507&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;span data-testid=&quot;inline-card-icon-and-title&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Custom Conatainer: &lt;a href=&quot;https://docs.ceph.com/en/reef/cephadm/services/custom-container/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.ceph.com/en/reef/cephadm/services/custom-container/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764166748531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        [Ceph MON / MGR / OSD]                [Custom Service (go-ceph API)]
                 (Core Ceph)                        (독립 컨테이너)
                       │                                   │
                       └────── Ceph Protocols ──────┬──────┘
                                 (librados, REST 등)&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;- Ceph 클러스터 내부에서 배포되지만, &lt;b&gt;Ceph의 핵심 데몬 (OSD, MON, MGR등)에 직접 속하지 않는 독립 실행형 컨테이너&lt;/b&gt;를 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Cephadm이 이서비스를 container 단위로 배포하고, 운영 관리를 도와주는 형태이지 Ceph 데몬처럼 클러스터의 핵심 구성요소는 아님&lt;br /&gt;&lt;br /&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;- Ceph 크러스터 외부에 &lt;b&gt;독립 실행형 컨테이너 서비스&lt;/b&gt;를 배포할 수 있는 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ceph orch apply 명령으로 배포하고 Ceph-orchestrator가 lifecycle을 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 특징&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Ceph core 역할은 아니지만 &lt;b&gt;Ceph 프로토콜(librados, REST)&lt;/b&gt;로 클러스터와&lt;b&gt; 느슨하게 연결 (loose coupling)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 배포는 &lt;b&gt;원하는 노드에 자유롭게&lt;/b&gt;, cephadm orchestrator가 관리&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;Ceph-MGR Module vs Custom Container Servcie&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #292a2e; text-align: start; border-collapse: collapse; width: 100%; height: 148px;&quot; border=&quot;1&quot; data-ke-style=&quot;style1&quot; data-ke-align=&quot;alignLeft&quot; data-testid=&quot;renderer-table&quot; data-number-column=&quot;false&quot; data-table-width=&quot;760&quot; data-layout=&quot;default&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 17px;&quot;&gt;MgrModule&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 17px;&quot;&gt;&lt;span&gt;Custom Container Service&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;164&quot;&gt;&lt;span&gt;&lt;b&gt;실행 위치&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;278&quot;&gt;&lt;span&gt;Ceph‑Mgr 데몬 내부&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;316&quot;&gt;&lt;span&gt;독립 실행 컨테이너&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;164&quot;&gt;&lt;span&gt;&lt;b&gt;언어 제약&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;278&quot;&gt;&lt;span&gt;Python only&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;316&quot;&gt;&lt;span&gt;언어 제한 없음 (Go, Java, Python 등)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;164&quot;&gt;&lt;span&gt;&lt;b&gt;클러스터와 연동도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;278&quot;&gt;&lt;span&gt;매우 밀접 (내부 기능)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;316&quot;&gt;&lt;span&gt;느슨한 결합 (REST, librados 등 사용)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;164&quot;&gt;&lt;span&gt;&lt;b&gt;배포 대상&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;278&quot;&gt;&lt;span&gt;모든 mgr 노드&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;316&quot;&gt;&lt;span&gt;지정한 노드에 선택 배포&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;164&quot;&gt;&lt;span&gt;&lt;b&gt;관리 주체&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;278&quot;&gt;&lt;span&gt;Ceph‑Mgr 데몬&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;316&quot;&gt;&lt;span&gt;cephadm orchestrator&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;164&quot;&gt;&lt;span&gt;&lt;b&gt;라이프사이클 관리&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;278&quot;&gt;&lt;span&gt;ceph mgr module enable/disable&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left; height: 19px;&quot; data-colwidth=&quot;316&quot;&gt;&lt;span&gt;ceph orch apply/update/remove&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선택 배경 및 결정 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 개발한 API는 go-ceph 기반으로 구현하였으며, Ceph 클러스터 내부의 상태 정보를 활용하는 구조입니다. 이 기능을 실 서비스 환경에 통합하기 위해 처음에는 Ceph-MGR Module을 고려했습니다. MGR Module은 Ceph 클러스터 내부 기능과 밀접하게 연동할 수 있다는 점에서 매력적이었지만, 몇 가지 현실적인 제약이 있었습니다.&lt;/p&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;MGR&amp;nbsp;Module은&amp;nbsp;Python&amp;nbsp;기반으로만&amp;nbsp;개발할&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;ceph-mgr&amp;nbsp;데몬&amp;nbsp;내부에서&amp;nbsp;동작하기&amp;nbsp;때문에&amp;nbsp;실제&amp;nbsp;Ceph&amp;nbsp;환경이&amp;nbsp;갖춰진&amp;nbsp;상태에서만&amp;nbsp;테스트가&amp;nbsp;가능합니다.&amp;nbsp;이로&amp;nbsp;인해&amp;nbsp;개발&amp;nbsp;및&amp;nbsp;디버깅&amp;nbsp;속도가&amp;nbsp;느려지고,&amp;nbsp;개발&amp;nbsp;환경&amp;nbsp;구축이&amp;nbsp;까다롭습니다.&lt;/p&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;cephadm&amp;nbsp;기반으로&amp;nbsp;클러스터를&amp;nbsp;운영하고&amp;nbsp;있는데,&amp;nbsp;이&amp;nbsp;환경에서는&amp;nbsp;mgr&amp;nbsp;데몬이&amp;nbsp;컨테이너로&amp;nbsp;실행되므로,&amp;nbsp;직접&amp;nbsp;소스&amp;nbsp;파일을&amp;nbsp;수정하거나&amp;nbsp;테스트하는&amp;nbsp;데&amp;nbsp;제약이&amp;nbsp;따릅니다.&amp;nbsp;예를&amp;nbsp;들어,&amp;nbsp;새로운&amp;nbsp;mgr&amp;nbsp;모듈을&amp;nbsp;반영하려면&amp;nbsp;컨테이너에&amp;nbsp;볼륨&amp;nbsp;마운트&amp;nbsp;설정을&amp;nbsp;추가하거나&amp;nbsp;재시작해야&amp;nbsp;하고,&amp;nbsp;ceph&amp;nbsp;mgr&amp;nbsp;module&amp;nbsp;enable/disable&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;이러한&amp;nbsp;이유들로&amp;nbsp;인해&amp;nbsp;결국&amp;nbsp;Custom&amp;nbsp;Container&amp;nbsp;Service를&amp;nbsp;선택하게&amp;nbsp;되었습니다.&amp;nbsp;Custom&amp;nbsp;Container는&amp;nbsp;Ceph&amp;nbsp;클러스터&amp;nbsp;외부에서&amp;nbsp;독립적으로&amp;nbsp;동작하는&amp;nbsp;서비스이기&amp;nbsp;때문에,&amp;nbsp;언어나&amp;nbsp;프레임워크에&amp;nbsp;제한이&amp;nbsp;없고,&amp;nbsp;개발&amp;nbsp;및&amp;nbsp;테스트도&amp;nbsp;로컬에서&amp;nbsp;쉽게&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;배포&amp;nbsp;또한&amp;nbsp;ceph&amp;nbsp;orch&amp;nbsp;apply&amp;nbsp;명령어&amp;nbsp;한&amp;nbsp;줄로&amp;nbsp;가능하며,&amp;nbsp;전체&amp;nbsp;라이프사이클을&amp;nbsp;Ceph의&amp;nbsp;orchestrator가&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;결과적으로 여러 요소를 고려했을 때, Custom Container Service가 우리 팀에게 더 적합한 방향이라고 판단되었고, 이에 따라 실제 구현과 배포를 진행하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과정&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 제 목표는 클러스터에 컨테이너가 잘 올라가는지 테스트를 하는게 목적이었기 때문에, go로 간단하게 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;&lt;b&gt;Dockerfile&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764167258009&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 빌드 단계
FROM --platform=linux/amd64 golang:1.24-bookworm AS builder
WORKDIR /app
LABEL ceph=&quot;true&quot;

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
  build-essential \
  librados-dev \
  librbd-dev \
  ceph-common \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags no_encryption -o ceph-core-api ./cmd/api

# 실행 단계
FROM --platform=linux/amd64 debian:bookworm-slim
WORKDIR /app
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y librados2 ceph-common &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/ceph-core-api .

EXPOSE 9080
CMD [&quot;./ceph-core-api&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ceph&amp;nbsp;클러스터와&amp;nbsp;통신할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;Go&amp;nbsp;기반&amp;nbsp;API를&amp;nbsp;효율적으로&amp;nbsp;빌드하고&amp;nbsp;실행하기&amp;nbsp;위해&amp;nbsp;멀티스테이지&amp;nbsp;빌드&amp;nbsp;방식을&amp;nbsp;사용했습니다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;불필요한&amp;nbsp;의존성을&amp;nbsp;제거한&amp;nbsp;경량&amp;nbsp;컨테이너&amp;nbsp;이미지를&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;&lt;b&gt;빌드&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-width=&quot;760&quot; data-has-width=&quot;true&quot; data-mode=&quot;wide&quot;&gt;
&lt;div style=&quot;background-color: #ffffff;&quot; data-local-id=&quot;8c9f1da8-56f0-40e6-8c7c-d764941babc4&quot;&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1764167323385&quot; class=&quot;oxygene&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;FROM --platform=linux/amd64 golang:1.24-bookworm AS builder&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;golang&amp;nbsp;이미지를&amp;nbsp;기반으로&amp;nbsp;Go&amp;nbsp;애플리케이션을&amp;nbsp;컴파일합니다.&lt;/li&gt;
&lt;li&gt;--platform=linux/amd64는&amp;nbsp;호스트&amp;nbsp;아키텍처와&amp;nbsp;관계없이&amp;nbsp;명확한&amp;nbsp;타깃&amp;nbsp;플랫폼을&amp;nbsp;지정해&amp;nbsp;M1/M2&amp;nbsp;Mac&amp;nbsp;등에서도&amp;nbsp;호환&amp;nbsp;문제를&amp;nbsp;방지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764167340881&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
  build-essential \
  librados-dev \
  librbd-dev \
  ceph-common \&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;Ceph 클러스터와 연동하기 위해 필요한 개발 헤더 및 라이브러리를 설치합니다.&lt;/li&gt;
&lt;li&gt;Go의&amp;nbsp;CGO&amp;nbsp;기능을&amp;nbsp;활용해&amp;nbsp;librados를&amp;nbsp;사용하기&amp;nbsp;위한&amp;nbsp;필수&amp;nbsp;패키지입니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764167448288&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags no_encryption -o ceph-core-api ./cmd/api&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;CGO를 활성화하여 Ceph C 라이브러리와 연동 가능하게 빌드합니다.&lt;/li&gt;
&lt;li&gt;-tags no_encryption은 필요한 경우 빌드 태그를 조정할 수 있습니다. (해당 태그가 없는 경우 빌드 에러)&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 id=&quot;code_1764167467622&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM --platform=linux/amd64 debian:bookworm-slim&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;실행&amp;nbsp;이미지는&amp;nbsp;debian-slim&amp;nbsp;기반으로,&amp;nbsp;빌드&amp;nbsp;도구&amp;nbsp;없이&amp;nbsp;필요한&amp;nbsp;라이브러리만&amp;nbsp;포함해&amp;nbsp;이미지&amp;nbsp;크기를&amp;nbsp;최소화합니다.&lt;/li&gt;
&lt;li&gt;Ceph&amp;nbsp;공식&amp;nbsp;컨테이너&amp;nbsp;이미지나&amp;nbsp;대부분의&amp;nbsp;Ceph&amp;nbsp;관련&amp;nbsp;컴포넌트는&amp;nbsp;linux/amd64를&amp;nbsp;기본으로&amp;nbsp;빌드됨&lt;/li&gt;
&lt;li&gt;호스트와&amp;nbsp;상관없이&amp;nbsp;항상&amp;nbsp;Ceph&amp;nbsp;클러스터와&amp;nbsp;호환되는&amp;nbsp;아키텍처로&amp;nbsp;이미지를&amp;nbsp;고정&amp;nbsp;(플랫폼을&amp;nbsp;맞춰주지&amp;nbsp;않는&amp;nbsp;경우&amp;nbsp;컨테이너&amp;nbsp;에러)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764167504441&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RUN apt-get update &amp;amp;&amp;amp; apt-get install -y librados2 ceph-common&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;런타임에서&amp;nbsp;librados를&amp;nbsp;활용하기&amp;nbsp;위해&amp;nbsp;필요한&amp;nbsp;패키지를&amp;nbsp;설치합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764167581864&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;COPY --from=builder /app/ceph-core-api .
EXPOSE 9080
CMD [&quot;./ceph-core-api&quot;]&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;빌드된&amp;nbsp;바이너리만&amp;nbsp;복사해&amp;nbsp;실행&amp;nbsp;환경을&amp;nbsp;구성하고,&amp;nbsp;9080&amp;nbsp;포트로&amp;nbsp;API를&amp;nbsp;제공하도록&amp;nbsp;설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;ca5fa1e6-8060-46e9-b5ad-6222c08850a7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Dockerfile로 이미지를 빌드하고 컨테이너 허브에 push&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764167631316&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker buildx build --platform linux/amd64 -t harbor.tareun.kr/share/djlee/ceph-core-api:v1.0.3 --push .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 저는 맥북(arm64)을 사용하기 때문에 --platform 옵션 없이 빌드하면 arm64 아키텍처 이미지로 생성됩니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2c2402fd-2d21-4b06-9315-e8eba4ab053a&quot; data-ke-size=&quot;size16&quot;&gt;arm64 아키텍처로 Ceph 클러스터에 배포하면 컨테이너 실행이 실패합니다.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2c2402fd-2d21-4b06-9315-e8eba4ab053a&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;2c2402fd-2d21-4b06-9315-e8eba4ab053a&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제 실제 Cluster에서 Push한 컨테이너 이미지를 사용해서 배포를 진행하면 됩니다.&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;db2cc612-3961-44fb-8128-6675ca4dea34&quot; data-ke-size=&quot;size23&quot;&gt;Spec 예시 및 옵션 (ceph-core-api.yml)&lt;/h3&gt;
&lt;pre id=&quot;code_1764167678869&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;service_type: container
service_id: ceph-core-api
placement:
  hosts:
    - ceph152 
    - ceph153 
spec:
  image: harbor.xxx.kr/share/djlee/ceph-core-api:v1.0.4
  args:
    - &quot;--net=host&quot;
  ports:
    - 9080
  envs:
    - GIN_MODE=release
  bind_mounts:
    - ['type=bind', 'source=/etc/ceph', 'destination=/etc/ceph', 'ro=true']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764167727788&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ceph orch apply -i ceph-core-api.yml&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 widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uTxOb/dJMcabo6gtd/Fkk5TEeA9gACbEYzEKy9ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uTxOb/dJMcabo6gtd/Fkk5TEeA9gACbEYzEKy9ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uTxOb/dJMcabo6gtd/Fkk5TEeA9gACbEYzEKy9ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuTxOb%2FdJMcabo6gtd%2FFkk5TEeA9gACbEYzEKy9ZK%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;1340&quot; height=&quot;468&quot; data-origin-width=&quot;1340&quot; data-origin-height=&quot;468&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;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764167788461&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; curl http://121.141.64.152:9080/api/cluster/fsid
 
 {
  &quot;status&quot;: &quot;success&quot;,
  &quot;data&quot;: {
    &quot;fsid&quot;: &quot;df5355e6-f66f-45c6-83b0-4680e086eff5&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erOTyI/dJMcadNV9Mx/icKCpC8NCoJURPIhCPLalK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erOTyI/dJMcadNV9Mx/icKCpC8NCoJURPIhCPLalK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erOTyI/dJMcadNV9Mx/icKCpC8NCoJURPIhCPLalK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerOTyI%2FdJMcadNV9Mx%2FicKCpC8NCoJURPIhCPLalK%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;1338&quot; height=&quot;848&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;87c97e0b-0da2-4aff-b647-ca57928bad50&quot; data-ke-size=&quot;size26&quot;&gt;Prometheus 연동&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-pm-slice=&quot;3 3 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;bulletList&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;Cephadm으로 관리되는 Prometheus에서 기본 Ceph exporter 외에 사용자 정의 메트릭 엔드포인트 수집하기 위함&lt;/li&gt;
&lt;li data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;listItem&quot; data-prosemirror-content-type=&quot;node&quot;&gt;기존 Ceph 모듈(ceph, ceph-exporter, node-exporter)의 scrape 설정 유지, 새로운 job 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;d1fe6f5a-387c-40b4-b303-5dca3ba05e5b&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 템플릿 파일 작성 (prometheus.yml.j2)&lt;/b&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;988a9441-b3f4-4673-ad80-32ab1c88c4b9&quot; data-ke-size=&quot;size16&quot;&gt;기존 job 설정을 모두 포함하지 않으면 기존 수집 항목이 사라질 수 있으니, 반드시 기존 job도 포함해야 함.&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;503b884e-1e50-44fa-8805-72fd4c1bb0f7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;: 템플릿을 작성하기 전에, 현재 Cephadm에서 Prometheus가 수집 중인 모듈(job)확인 필수!&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764167966507&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'ceph'
    static_configs:
      - targets: ['ceph155:9283']

  - job_name: 'ceph-exporter'
    static_configs:
      - targets:
          - 'xxx.xxx.xx.xxx:9926'
          - 'xxx.xxx.xx.xxx:9926'
          - 'xxx.xxx.xx.xxx:9926'
          - 'xxx.xxx.xx.xxx:9926'

  - job_name: 'node'
    static_configs:
      - targets:
          - 'xxx.xxx.xx.xxx:9100'
          - 'xxx.xxx.xx.xxx:9100'
          - 'xxx.xxx.xx.xxx:9100'
          - 'xxx.xxx.xx.xxx:9100'

  - job_name: 'ceph-core-api'
    static_configs:
      - targets:
          - 'localhost:9080'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;6fd1de9f-0018-41e8-b20d-37d9624e2e1d&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. config-key로 등록&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764167985791&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ceph config-key set mgr/cephadm/services/prometheus/prometheus.yml -i prometheus.yml.j2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;9b7ea090-e801-473c-a312-fcd67ee05b76&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Prometheus 서비스 재배포&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764168023577&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ceph orch redeploy promoetheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;9b7ea090-e801-473c-a312-fcd67ee05b76&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;heading&quot; data-prosemirror-content-type=&quot;node&quot; data-local-id=&quot;9b7ea090-e801-473c-a312-fcd67ee05b76&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 결과&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;1318&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwxaKe/dJMcahiydHk/h6iWTrB3xvZDv24zvHtql1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwxaKe/dJMcahiydHk/h6iWTrB3xvZDv24zvHtql1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwxaKe/dJMcahiydHk/h6iWTrB3xvZDv24zvHtql1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwxaKe%2FdJMcahiydHk%2Fh6iWTrB3xvZDv24zvHtql1%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;1318&quot; height=&quot;958&quot; data-origin-width=&quot;1318&quot; data-origin-height=&quot;958&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmrF63/dJMb99Y7FGN/fLK4JZ50MTu1gMSTu7FpK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmrF63/dJMb99Y7FGN/fLK4JZ50MTu1gMSTu7FpK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmrF63/dJMb99Y7FGN/fLK4JZ50MTu1gMSTu7FpK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmrF63%2FdJMb99Y7FGN%2FfLK4JZ50MTu1gMSTu7FpK0%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;1332&quot; height=&quot;602&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>dev/Ceph</category>
      <category>ceph</category>
      <category>CustomContainer</category>
      <category>Prometheus</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/112</guid>
      <comments>https://jhost.tistory.com/112#entry112comment</comments>
      <pubDate>Wed, 26 Nov 2025 23:47:20 +0900</pubDate>
    </item>
    <item>
      <title>실행 파일을 생성하는 링커</title>
      <link>https://jhost.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jhost.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.07.27 - [CS] - 소스코드파일부터 실행파일까지 컴파일러 과정 알아보기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1722062685150&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;소스코드파일부터 실행파일까지 컴파일러 과정 알아보기&quot; data-og-description=&quot;여러분들은 개발을 진행하면서 단순히 소스코드를 작성하게 되고, 개발한 소스코드를 토대로 실행파일이 생성이 됩니다.어떻게 우리가 작성한 소스코드가 실행파일이 되는지 알아가보겠습니&quot; data-og-host=&quot;jhost.tistory.com&quot; data-og-source-url=&quot;https://jhost.tistory.com/110&quot; data-og-url=&quot;https://jhost.tistory.com/110&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bHwFA6/hyWGO12l4Q/b8JVeDKr3GdPL9bG6Jcyb1/img.jpg?width=800&amp;amp;height=1027&amp;amp;face=0_0_800_1027,https://scrap.kakaocdn.net/dn/coFdDk/hyWGWsfAyE/9mAs8aiOrt5hLgqgiSQK41/img.jpg?width=800&amp;amp;height=1027&amp;amp;face=0_0_800_1027&quot;&gt;&lt;a href=&quot;https://jhost.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jhost.tistory.com/110&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bHwFA6/hyWGO12l4Q/b8JVeDKr3GdPL9bG6Jcyb1/img.jpg?width=800&amp;amp;height=1027&amp;amp;face=0_0_800_1027,https://scrap.kakaocdn.net/dn/coFdDk/hyWGWsfAyE/9mAs8aiOrt5hLgqgiSQK41/img.jpg?width=800&amp;amp;height=1027&amp;amp;face=0_0_800_1027');&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;소스코드파일부터 실행파일까지 컴파일러 과정 알아보기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;여러분들은 개발을 진행하면서 단순히 소스코드를 작성하게 되고, 개발한 소스코드를 토대로 실행파일이 생성이 됩니다.어떻게 우리가 작성한 소스코드가 실행파일이 되는지 알아가보겠습니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jhost.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대상 파일(object file) 생성 과정에 대해서 궁금하신 분은 이전에 작성한 글을 참고해 주시면 됩니다.&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;/h3&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;이전 글에서 말씀드린 컴파일러를 통해서 대상 파일(object file)이 생성된다는 걸 알았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러가 생성한 이 대상파일 파일을 여러개를 하나로 묶어서 최종 실행 파일을 생성해야 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 파일을 생성하는 과정인 &lt;b&gt;링커(linker)&lt;/b&gt;에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드 (func.c) -&amp;gt; 컴파일 (func.o)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;func.o라는 이름의 기계명령어에 해당하는 코드를 저장하는 파일이 생성되고, 이 파일을 &lt;b&gt;대상 파일(object file)&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 응용 프로그램부터 웹 브라우저나 웹 서버 같은 복잡한 응용프로그램들은 윈도에서 흔히 보이는 exe 형식의 실행 파일이나 리눅스의 elf 파일 같은 실행 파일은 링커가 필요한 대상 파일을 한데 모아 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;링커 작업 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크의 전체 과정은 저자 여러명이 각각 특정 부분을 맡아 챕터별로 따로 집필하고, 개별 장을 묶어 책 한 권으로 출판하는 것과 비슷합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 심벌 해석(symbol resolution)&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;책의 특정 장이 다른 장의 내용을 참고할 때가 있는데, 이것은 우리가 작성하는 프로그램이 다른 모듈의 프로그래밍 인터페이스(programming interface) 또는 변수(variable)를 참조하는 것과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 list.c에서 일종의 연결 리스트(linked list)를 구현하고 다른 모듈에서 그 연결 리스트를 사용해야 한다면 이때 이 두 모듈 사이에 의존성이 있다고 말하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 링커가 하는 일 중 하나는 의존성(종속성)이 올바르게 설정되어 있는지, 다시 말해 인터페이스 구현이 의존성이 있는 모듈에서 사용 가능한지 확인하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 한 권이 오류 없이 완성되려면 서로 참고한 내용이 실제로 그 책 안에 정리가 되어 있어야겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 참조하고 있는 외부 심벌(external symbol)에 대한 실제 구현이 어느 모듈이든지 단 하나만 있어야 하는데, 링커는 이를 찾아내 연결하는 작업을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 재배치(relocation)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 어떤 하나의 장에서 다른 장의 내용을 인용할 때 몇 페이지의 내용이라고 언급해야 하는데, 각 저자가 집필할 때는 어느 페이지에 인용한 내용이 들어가는지 미리 알 수 없기 때문에 (책이 아직 완성된 상태가 아니기 때문에) 임시로 N 쪽이라고 표시하고 이후 책이 최종적으로 편집 작업에 들어가야 N 쪽이 몇 쪽인지 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU에&amp;nbsp;대한&amp;nbsp;자세한&amp;nbsp;설명은&amp;nbsp;N 쪽을&amp;nbsp;참고하세요.&amp;nbsp;-&amp;gt;&amp;nbsp;재배치&amp;nbsp;-&amp;gt; CPU에 대한 자세한 설명은 150쪽을 참고하세요.&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;이 과정에서 N 쪽이라고 표시된 부분을 전부 150쪽이라고 치환하는 과정을 재배치라고 합니다.&lt;/p&gt;
&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;특정한 소스 파일에서 다른 모듈에 정의되어 있는 함수를 참조할 때, 컴파일러가 이 소스 파일을 컴파일하는 시점에서는 이 함수가 어느 메모리 주소(memory address)에 위치할지 정확히 알 수 없기 때문에 컴파일러는 이 함수를 N으로 표시해 두고 일단 넘어가게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 링크 과정에서 링커가 N으로 표시되어 있는 곳들을 확인하고 모아 실행 파일을 생성하는 과정에서 함수의 정확한 주소를 확인하고 실제 메모리 주소로 대체합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 저자가 자신 분량을 모두 집필하고 나면 이를 모아 하나로 합쳐야 책 한 권이 완성될 수 있는데, 이렇게 완성된 책 한 권은 링크 과정을 마친 후 최종적으로 생성된 실행 파일에 비유할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역 변수는 모듈 내에서만 사용되어 외부 모듈에서 참조할 수 없기 때문에 링커의 관심 대상이 아니죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 링커가 실제로 관심을 갖는 것은 전역 변수입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 소스 파일에 다른 모듈에서 참조할 수 있는 심벌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 소스 파일이 다른 모듈에서 정의한 심벌 두 개를 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 링커는 위 두 가지 정보를 어떻게 알 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 컴파일러를 통해서 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 소스 코드를 기계 명령어로 번역하고 그 정보를 대상 파일에 저장하는데, 기계 명령어 생성할 뿐만 아니라 이 명령어를 실행시키는 데이터도 생성합니다. 이 데이터는 대상 파일에 반드시 포함이 되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 대상 파일에는 코드영역, 데이터 영역이 포함됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 영역 : 소스 파일에 정의된 함수에서 변환된 기계 명령어가 저장되는 부분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 영역 : 소스 파일의 전역 변수가 저장되는 부분&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(지역 변수는 프로그램이 실행된 후 스택 영역에서 생성되고 사용하면 제거되기 때문에 대상 파일에는 별도로 저장되지 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러가 링커 부담을 조금이나마 줄여주고자 추가적으로 하는 작어비 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 파일마다 외부에서 참조 가능한 심벌이 어떤 것인지 그 정보를 기록하고, 반대로 어떤 외부 심벌을 참조하고 있는지 기록하여 링커에게 전달해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 심벌 정보를 기록하는 표를 &lt;b&gt;심벌 테이블이라고&lt;/b&gt; 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zpwJR/btsIQGcy3cX/RvbDrFWnKCLmCzG0keDkt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zpwJR/btsIQGcy3cX/RvbDrFWnKCLmCzG0keDkt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zpwJR/btsIQGcy3cX/RvbDrFWnKCLmCzG0keDkt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzpwJR%2FbtsIQGcy3cX%2FRvbDrFWnKCLmCzG0keDkt0%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;240&quot; height=&quot;910&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 대상 파일에 코드 영역, 데이터 영역, 심벌 테이블이 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링커는 컴파일러를 통해 필요한 정보를 얻고, 공급과 수요를 충족하는지 확인합니다.&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;gt; 실제로 사용하지 않는 함수를 정의할 수는 있지만 존재하지 않는 함수를 사용할 수는 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재배치 과정에 대해서 자세히 알아보면, 재배치는 실행 시 주소를 정확한 주소지를 결정해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링커는 실행 파일을 생성할 때 프로그램이 실행되는 시점에 함수가 적재될 메모리 주소를 확정해야 하는데 이 메모리 주소는 어떻게 알 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러가 메모리 주소를 확정할 수 없는 변수를 발견할 때마다 . relo.text, . relo.data 파일을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.relo.text : 해당 명령어를 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.relo.data : 해당 명령어와 관련된 데이터를 저장&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZavZ6/btsIPjiAWGq/WXviwEheySkweRhb1ERCpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZavZ6/btsIPjiAWGq/WXviwEheySkweRhb1ERCpk/img.png&quot; data-alt=&quot;최종 대상 파일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZavZ6/btsIPjiAWGq/WXviwEheySkweRhb1ERCpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZavZ6%2FbtsIPjiAWGq%2FWXviwEheySkweRhb1ERCpk%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;240&quot; height=&quot;490&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;952&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종 대상 파일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;대상 파일에서 각 유형의 영역이 모두 결합되면 모든 기계 명령어와 전역 변수가 프로그램 실행 시간에 위치할 메모리 주소를 결정할 수 있습니다. 링커는 각 대상 파일의. relo.text 영역을 읽어 명령어를 확인하고, 심벌의 코드 영역 시작 주소 값을 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초 주소는 0x00으로 표시되고,. relo.text,. relo.data를 읽어 들여 심벌의 코드 영역 시작 주소 기준 오프셋을 확인하고 실행 파일에서 해당 명령어를 찾아 이동할 소스 주소를 0x00에서 이동할 주소로 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 심벌의 메모리 주소를 수정하는 과정이 &lt;b&gt;재배치&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로그램마다 변수나 명령어의 메모리 주소는 전부 다를 텐데, 링커가 프로그램이 실행된 후의 변수나 기계 명령어의 메모리 주소를 확인할 수 있는 이유는 무엇일까요? 이는 &lt;b&gt;가상 메모리&lt;/b&gt;와 관련이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev/CS</category>
      <category>링커</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/111</guid>
      <comments>https://jhost.tistory.com/111#entry111comment</comments>
      <pubDate>Sat, 27 Jul 2024 16:29:52 +0900</pubDate>
    </item>
    <item>
      <title>소스 코드 파일부터 실행 파일까지 컴파일러 과정 알아보기</title>
      <link>https://jhost.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;여러분들은 개발을 진행하면서 단순히 소스코드를 작성하게 되고, 개발한 소스코드를 토대로 실행파일이 생성이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 우리가 작성한 소스코드가 실행파일이 되는지 알아가보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인간이 인식할 수 있는 단어로 코드를 작성하는 것 == 소스 파일 (source file)&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;gt; 컴파일러 -&amp;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;컴파일러는 고급 프로그래밍 언어를 저급 프로그래밍(CPU가 인식할 수 있는) 언어로 바꿔주는 역할을 하며, 컴파일된 이후의 코드를 목적코드(object code)라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 컴파일러를 사용할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 코드 예시&lt;/p&gt;
&lt;pre id=&quot;code_1722060945328&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int a = 1;
int b = 2;

while(a &amp;lt; b)
{
	b = b - 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인간의 관점&lt;/p&gt;
&lt;pre id=&quot;code_1722060988762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a 변수에 1을 할당합니다;
b 변수에 2를 할당합니다;
a &amp;lt; b 이면 b가 1씩 줄어듭니다;
더 이상 a &amp;lt; b가 성립하지 않을 때까지의 앞의 문장을 반복합니다;&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;CPU는 이런 추상적인 표현을 직접 이해할 수 없기 때문에 컴파일러의 도움을 받아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 작성한 소스코드는 컴파일러를 통해 CPU가 인식할 수 있는 언어로 변환이 되어 우리가 작성한 소스코드대로 동작이 진행됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(CPU는 단세포 생물처럼 매우 원시적이며 멍청하지만, 이 멍청함을 상쇄하고도 남을 정도고 속도가 매우 빠릅니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;컴파일러 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째, 어휘 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 먼저 각 항목을 잘게 쪼개고, 각 항목이 가지고 있는 추가 정보를 묶어서 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러의 첫 번째 작업은 소스 코드를 돌아다니면서 모든 토큰을 찾아냅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1722061178784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;T_Keyword	int
T_Identifier	a
T_Assign	= 
T_Int	1
T_Semicolon	:
T_Keyword	int
T_Identifier	b
T_Assign	=
T_Int	2&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;1. 키워드&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. int 키워드라는 추가 정보를 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 항목에 추가로 정보를 결헙한 것을 전문용어로 토큰(token)이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 줄은 하나의 토큰을 의미하며, T로 시작하는 왼쪽 열은 토큰을 의미하며, 오른쪽 열은 각각의 토큰이 가지는 값을 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 소스 코드에서 토큰을 추출하는 과정을 &lt;b&gt;어휘 분석&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째, 구문 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 구문에 따라 한 글자도 놓치지 않고 해석(parsing) 작업을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 컴파일러가 while 키워드의 토큰을 찾으면 다음 토큰이 (라는 것을 알 수 있는데, 다음 토큰이 while 키워드에 필요한 토큰이 아니라면 컴파일러는 문법 오류(syntax error)를 보고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러는 구문에 따라 해석을 하며 트리를 생성하고, 이 트리를 생성하는 전체 과정을 &lt;b&gt;구문 분석&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 번째, 의미 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구문 분석을 통해 구문 트리가 생성되고 나면 구문 트리에 이상이 없는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 정수 값에 문자열을 더하면 안 되고, 비교 기호의 좌우에 있는 값 형식이 다른 경우를 찾으며 이 단계를 통과하면 작성한 소스코드에 이상이 없기 때문에 컴파일 오류가 없다는 것이 증명됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 &lt;b&gt;의미 분석&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네 번째, 중간 코드 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미 분석이 끝나면 컴파일러는 구문 트리를 탐색한 결과를 바탕으로 좀 더 다듬어진 형태인 중간 코드(Intermediate Representation Code, IR Code)를 생성합니다.&lt;/p&gt;
&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_1722061739118&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a = 1
b = 2
goto B
A: b = b - 1
B: if a &amp;lt; b goto A&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;pre id=&quot;code_1722061891808&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;movl $0x1, -0x4(%rbp) // a = 1
movl $0x2, -0x8(%rbp) // b = 2
jmp B // b로 점프
A: subl $0x1, -0x8(%rbp) // b = b - 1
B: mov -0x4(%rbp),%eax
cmp -0x8(%rbp),%eax // a &amp;lt; b ?
jl A	// a &amp;lt; b 이면 A로 점프&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여섯 번째, 기계 명령어로 변환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 컴파일러는 이 어셈블리어 코드를 기계 명령어로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 컴파일러는 인간이 작성한 소스코드라고 부르는 문자열을 CPU가 실행할 수 있는 기계 명령어로 변환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 지금 까지 과정이 끝나면 실행 파일이 알아서 생성이 되는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 추가적으로 대상 파일을 병합하는 링크(link) 작업이 필요합니다.&lt;/p&gt;
&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;이 링크를 담당하는 프로그램을 링커(linker)라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터&amp;nbsp;밑바닥의&amp;nbsp;비밀&lt;/p&gt;</description>
      <category>dev/CS</category>
      <category>컴파일러</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/110</guid>
      <comments>https://jhost.tistory.com/110#entry110comment</comments>
      <pubDate>Sat, 27 Jul 2024 15:41:48 +0900</pubDate>
    </item>
    <item>
      <title>항해 플러스 4기 백엔드 솔직 후기</title>
      <link>https://jhost.tistory.com/109</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2584&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btnUpD/btsHMyl8QQF/qplWyisqSK3w4YmtHyUEl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btnUpD/btsHMyl8QQF/qplWyisqSK3w4YmtHyUEl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btnUpD/btsHMyl8QQF/qplWyisqSK3w4YmtHyUEl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtnUpD%2FbtsHMyl8QQF%2FqplWyisqSK3w4YmtHyUEl0%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;2584&quot; height=&quot;1340&quot; data-origin-width=&quot;2584&quot; data-origin-height=&quot;1340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 항해 플러스 4기 10주간의 여정이 모두 마무리되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10주 동안 많은 일이 있었지만 내용을 정리할 겸 회고를 작성해보려고 합니다&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 data-ke-size=&quot;size16&quot;&gt;저는 현재 3년 차 그룹웨어 서비스에서 웹 개발자로 재직 중이며, 개발자가 되기 전에 학부 시절 개발에 대해서 관심을 가지게 되었고 졸업 후 국비학원을 통해서 개발자로 취업하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 회사에서는 솔루션 회사이다 보니 기존에 이미 잘 만들어져 있는 소스코드 기반 위에서 작업을 하기 때문에 타 회사에서 사용하는 기술들에 비해서 많이 노후된 기술들을 사용하기 때문에 업무시간 외에 혼자서 공부를 하는데 어느 정도 한계가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;우연히 항해 플러스 광고를 보게 되었는데, 항해 플러스에 들어오기 전에 제가 가장 고민이 되었던 부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;물경력&lt;/b&gt;이라는 단어가 제일 눈에 들어왔습니다. 3년 차가 된 만큼 저 자신 스스로에게 변화가 필요하다고 느끼게 되어서 커리큘럼을 보고 실제 서비스 회사에서 사용하는 기술들을 보고 &quot;10주간 한번 미친 듯이 해보자&quot;라는 생각을 가지고 나름 열심히 참석했던 것 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;항해 플러스 백엔드 코스의 장점&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HYf9G/btsHMdJi8mM/IFrqHvtSeUdfkwzdKqpYx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HYf9G/btsHMdJi8mM/IFrqHvtSeUdfkwzdKqpYx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HYf9G/btsHMdJi8mM/IFrqHvtSeUdfkwzdKqpYx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHYf9G%2FbtsHMdJi8mM%2FIFrqHvtSeUdfkwzdKqpYx0%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;226&quot; height=&quot;1126&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 진행되는 커리큘럼!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;본 과정은 TDD, 클린아키텍처 기반 서버 구축, 통합 모니터링 시스템 구축, 대용량 트래픽 처리 등 매주 다양한 발제 주제들을 통해서 한 주마다 2번의 과제 제출로 진행하게 되는데 이런 과제들을 해결해 나가면서 퍼즐조각을 하나씩 맞춰졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 제일 흥미롭게 진행했던 주제는 클린 아키텍처 서버 구축 + 대용량 트래픽입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버구축 발제를 하면서 본인이 원하는 API를 선택해서 진행하게 되는데 대기열 프로세스를 구현하는 API를 선택해서 개발하면서 REDIS사용 경험이 부족했던 저는 RDB를 사용해서 개발을 하다가 동시성 테스트를 통과하기 위해 지속적인 리팩토링 과정을 통해서 많은 성장을 했다고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;멘토링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄청난 커리어를 가지신 시니어 멘토들과 발제 주제에 대해서 같이 고민하면서 해결해 나갈 수 있었고, 발제 주제가 아니더라도 다른 기술적인 고민이 있을 면 진심으로 같이 고민해 주시면서 해결책을 찾는 방법을 알려주셨습니다. 멘토링 시간이 매주 각 조마다 1시간 밖에 안되지만 항상 1시간 넘게 진행해 주시고 다른 조들의 멘토링 시간을 도강하면서 다른 사람들은 이런 고민도 하는구나 하면서 다양한 경험을 할 수 있는 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리뷰&lt;/b&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;pass/fail 여부를&amp;nbsp;&lt;/span&gt;확인할 수 있으며, 과제에 대한 코치님들의 자세한 피드백도 확인이 가능합니다. 그리고 다른 동료들은 어떤 고민을 가지고 어떤 식으로 해결했는지 공유가 되는 부분도 정말 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 팀끼리만 공유하는 게 아니라 모든 동료들과 같이 공유하며 매주 토요일 랜덤 피어 리뷰를 진행하게 됩니다. 이러한 시간을 통해서 다양한 관점에서도 생각할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워킹&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해 플러스 과정의 또 다른 매력은 혼자서 하는 것이 아니라 팀원들과 함께 여정을 떠나면서 서로 편하게 피드백을 주고받으면서 같이 성장해 나간다는 부분도 정말 좋았습니다. 앞으로 같이 성장해 나갈 정말 좋은 동료들을 얻었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;10주간 이 과정을 마무리했다고 해서 끝나는 것이 아닌 이 과정을 통해서 발판을 만들었고, 저는 이제 이 발판을 통해서 더욱 높게 올라가기 위해 꾸준히 공부를 할 생각입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항해 플러스에 투자한 시간과 돈은 이 과정을 끝내면서 전혀 아깝다는 생각이 들지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200만 원 가까이하는 비싼 금액이지만, 그 이상의 값어치를 한다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나의 장점은 발제에 대한 녹화본도 제공이 되기 때문에 부족한 부분에 대해서 스스로 복습할 수도 있고 수료 이후에도 선배 기수 역할로 한번 더 참여할 수 있는 기회도 제공되며, 멘토링을 참관하며 최신 문서로 복습할 수 있는 기회도 주어집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 상황 중에 3가지 이상 해당이 되신다면 항해 플러스 코스는 도움이 된다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개발자로 현재 일을 하고 있는데 개발 외의 업무가 더 많다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 주변에 아는 개발자가 없어서 기술적으로 커뮤니케이션할 동료가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스스로의 개발 실력이 부족하다고 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 내가 지금 잘하고 있는 건지 판단이 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 미래 커리어에 대한 고민&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 회사에서 쓰는 기술스택이 오래된 기술 스택인 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 시니어 개발자가 없거나 회사의 시스템이 미비한 환경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특별할인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 동료들과 네트워킹 하면서 소중한 경험을 쌓고 싶은 주니어 개발자라면 항해 플러스 백엔드 과정 신청하는 것을 적극 추천드립니다. 항해 플러스 과정의 중요한 장점은 공동 학습 및 네트워킹을 통해서 스스로의 학습능력을 키울 수 있는 점! 과정 전반에 걸쳐 다양한 동료들과 협력하고, 경험이 풍부한 코치와 함께 멘토링을 진행하게 되면서 개발자로서 성장하는데 도움이 될 뿐만 아니라 앞으로의 경력이 충분한 도움이 될 거라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 항해 플러스 백엔드 코스가 관심 있으시다면 지인추천 제도를 이용하시면 &lt;u&gt;특별할인(등록금 할인)&lt;/u&gt;도 있다고 하니 관심 있으신 분은 코드를 통해서 지원하시기 바랍니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 : ZOpfQY&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;모든 개발자분들 화이팅입니다!!!!!!!!!!!!!!!!!!!!!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;308&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjDFDG/btsHLWuf84G/6tVqax6DYHkQDGEvBNqd61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjDFDG/btsHLWuf84G/6tVqax6DYHkQDGEvBNqd61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjDFDG/btsHLWuf84G/6tVqax6DYHkQDGEvBNqd61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjDFDG%2FbtsHLWuf84G%2F6tVqax6DYHkQDGEvBNqd61%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;308&quot; height=&quot;163&quot; data-origin-width=&quot;308&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>항해</category>
      <category>항해백앤드</category>
      <category>항해솔직후기</category>
      <category>항해플러스</category>
      <category>항해플러스백앤드</category>
      <category>항해플러스백엔드</category>
      <category>항해플러스후기</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/109</guid>
      <comments>https://jhost.tistory.com/109#entry109comment</comments>
      <pubDate>Sun, 2 Jun 2024 19:29:11 +0900</pubDate>
    </item>
    <item>
      <title>항해 플러스 9주차 회고</title>
      <link>https://jhost.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;9주차 &lt;b&gt;책임 분리를 통한 애플리케이션 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.  문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직과 트랜잭션 범위 고려&lt;/p&gt;
&lt;pre id=&quot;code_1716113830379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
void payment() {
    결제_요청_검증()
    유저_포인트_차감()
    결제_정보_저장()
    주문_정보_전달() //외부 플랫폼 API 호출    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 상품을 결제하는 결제 함수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제처리 하는 비즈니스 로직 안에 여러 가지 함수들이 존재하며, 결제 처리가 끝난 후 최종적으로 주문 정보를 외부 API를 호출하여 전송해 주는 기능까지 존재한다. 즉, payment()가 정상적으로 성공 처리 되기 위해서는 외부 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;위에 로직은 정상적인 것 같지만 심각한 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 외부 플랫폼 API 호출 시 네트워크 이슈로 인해 통신이 오래 걸리는 경우 pamyent()로직 자체가 오래 걸리게 된다. (하나의 트랜잭션으로 동작하기 때문에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- payment()가 실행되면서 모든 로직이 성공적으로 다 처리 되었지만 외부 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;주문 정보 전달은 부가 로직 이므로 주요 로직인 결제 처리에 영향을 끼치면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 시도 &amp;amp; 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점을 해결하기 위해서 이벤트를 사용해서 부가 로직과 주요 로직의 결합을 끊어낼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716114767055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
void payment() {
    결제_요청_검증()
    유저_포인트_차감()
    결제_정보_저장()
    applicationEventPublisher.publishEvent(주문_정보_전달()) //외부 플랫폼 API 호출    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 제공하는 이벤트리스너 기 등을 사용해서 이벤트로 비즈니스 로직을 수정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트의 동작 과정 : 이벤트 생성 주체 -&amp;gt; 이벤트 디스패처(이벤트 퍼블리셔) -&amp;gt; 이벤트 핸들러(이벤트 구독자)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 이벤트를 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 이벤트를 사용해서 부가 로직(외부 API)에 의해서 주요 로직의 영향이 없도록 만들 수 있지만!!!! 단순히 이벤트로 처리한다고 해서 영향이 없는 건 아니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트는 크게 동기 방식과 비동기 방식이 있는데 동기 방식으로 하는 경우에는 하나의 트랜잭션 범위로 동작하기 때문에 여전히 외부 서비스에 영향을 받게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 방식은 (이벤트 리스너에서 @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;비동기 방식을 사용하기 위해서는 @EnableAsync를 통해서 비동기를 활성화시켜야 한다!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;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;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;카프카를 적용하면서 만약 메시지 발행에 대해서 실패하는 경우? 에 대한 고민을 해결해 보기 위해서 Transactional Outbox Parttern을 사용하는 것을 보고 대략적으로 설계는 해봤지만 아직 실제로 적용해보진 못했다. 이 부분도 카프카를 공부해 보면서 나중에 적용해 볼 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 목표&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카를 (찍먹!?)&amp;nbsp;사용해봤지만.. 대용량 트래픽을 수월하게 카프카에 대한 필요성과 중요성이 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;조만간 아파치 카프카 애플리케이션 프로그래밍 with 자바 정독을 목표로 잡고 카프카에 대해서 제대로 알아 가는 시간이 필요할 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.yes24.com/Product/Goods/99122569&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.yes24.com/Product/Goods/99122569&lt;/a&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>항해99</category>
      <category>항해플러스</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/108</guid>
      <comments>https://jhost.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 19 May 2024 19:53:34 +0900</pubDate>
    </item>
    <item>
      <title>항해플러스 8주차 회고</title>
      <link>https://jhost.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주차는 콘서트 프로젝트를 진행하면서 대기열을 기존에는 RDB를 사용하여 구성하였으면, REDIS를 사용해서 구현하기 위해 다시 설계하고 구현해 보는 시간을 가졌다.&lt;/p&gt;
&lt;h1&gt;대기열 리팩토링&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열은 기존에 동시성 유즈 케이스 고민하면서 사용했던 Redisson 라이브러리를 활용해서 Redis를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열은 두 가지 상태를 가지고 있으며, 서비스 이용을 위해 대기하거나, 서비스를 이용할 수 있고, 대기열 만료 시간이 지난 사용자는 대기열에서 삭제가 되기 때문에 별도의 상태를 관리하지 않는 구조로 설계 방향을 잡았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Wating Queue&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;redissonClient.getScoredSortedSet(&quot;waitQueue&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis Sorted Set 자료 구조를 기반으로 만들어진 RScoredSortedSet 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waitQueue.add(System.currentTimeMillis(), userId);&lt;/code&gt; Key는 userId, Score는 요청 시간을 저장&lt;/li&gt;
&lt;li&gt;add 명령어는 내부적으로 Redis ZADD 명령어로 신규 대기열을 추가하고, RANK 대기열 순번을 반환&lt;/li&gt;
&lt;li&gt;대기열에 이미 존재하는 사용자가(활성화되지 않은 사용자)가 대기열을 다시 신청하는 경우 순번은 맨 뒤로 밀리게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Ongoing Queue&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;redissonClient.getMapCache(&quot;ongoingQueue&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis Hash 자료 구조를 기반으로 만들어진 RMapCache 사용&lt;/li&gt;
&lt;li&gt;Hash와 차이점은 Hash는 개별 요소 대해 TTL 설정이 불가능 하지만, RMapCache는 개별 요소에 대해 TTL 사용이 가능하다.(대기열 만료시간)&lt;/li&gt;
&lt;li&gt;콘서트 최종 결제가 성공한 사용자는 OngoingQueue에서 제거&lt;/li&gt;
&lt;/ul&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;code&gt;QUEUE_LIMIT - iQueueRedisRepository.getOngoingCount()&lt;/code&gt; 대기열 진입 가능한 수 계산&lt;/li&gt;
&lt;li&gt;대기열 진입 가능 한 수만큼 &lt;code&gt;waitQueue.pollFirst(availableQueueCount). stream(). toList();&lt;/code&gt; poll() -&amp;gt; 내부적으로 Redis &lt;code&gt;ZRANGE&lt;/code&gt; 명령어를 사용해서 1번째 순번부터 availableQueueCount까지 삭제&lt;/li&gt;
&lt;li&gt;waieQueue에서 삭제된 userId들을 OngoingQueue로 삽입하며 대기열 만료 시간만큼 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대기열 설계 방향성 선정 과정&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;대기열 활성화의 경우 TTL 없이 OngoingQueue에서 만료된 토큰의 수만큼 WaitingQueue에서 전환되는 경우&lt;/li&gt;
&lt;/ol&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;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;N초마다 M개의 토큰을 OngoingQueue로 전환하는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대기열 고객에게 서비스 진입 가능 시간을 대체로 보장할 수 있지만&lt;/li&gt;
&lt;li&gt;서비스를 이용하는 사용자의 수가 보장될 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방식에서 가장 큰 문제점인 앞사람이 완료하지 않으면 뒷사람이 들어갈 수 없는 문제를 해결하기 위해 각 사용자에 대하여 평균적인 시간(한 유저가 콘서트 조회를 시작한 이후에 하나의 예약을 완료할 때까지 걸리는 시간)을 고려하여 대기열 만료시간을 설정, 두 번째 방식에서 적절한 동시 접속자를 유지하기 위해서 서버 트래픽의 최대치를 고려하여 &lt;code&gt;QUEUE_LIMIT&lt;/code&gt;로 동시 접속자 제한을 두어 처리하는 방향으로 1번과 2번 케이스에 대한 장단점을 최대한 살려서 가져가는 방식으로 설계를 진행했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Hash는 기본적으로 개별 요소에 대한 TTL 설정이 불가능하기 때문에 내가 설계한 방향대로 구현하기 위해서 각 사용자마다 Hash를 생성해서 그 Hash에 대한 TTL을 걸어줘야 하는데, Hash를 생성하는 만큼 Redis에 대한 부하가 많이 심해질 거라고 예상이 되기 때문에 순수 Redis를 사용하는 경우에는 어떤 식으로 설계할지 고민해봐야 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson에서 제공해 주는 RMapCache는 개별 요소에 대한 TTL을 어떤 식으로 제공을 하는 건지 공부를 해봐야 할 것 같고, 순수 Redis를 사용하는 경우에는 어떤 식으로 설계할지 고민이 필요해 보인다.&lt;/p&gt;</description>
      <category>회고</category>
      <category>항해99</category>
      <category>항해플러스</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/107</guid>
      <comments>https://jhost.tistory.com/107#entry107comment</comments>
      <pubDate>Sat, 11 May 2024 17:10:13 +0900</pubDate>
    </item>
    <item>
      <title>항해 플러스 5주차 회고</title>
      <link>https://jhost.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 항해 플러스 과정이 절반이 지났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주 차 동안 TTD, 아키텍처, 서버구축 등 과제를 하면서 정신없이 달려왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(절반 이상의 시간이 그동안 배운 내용들에 대해서 정리해야 될 것 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주차 발제 과제였던 콘서트 프로젝트를 진행하면서 필요한 도메인이 무엇인지 생각하고 도메인 중심적으로 개발을 진행하려고 했지만, 막상 개발을 하면서 데이터를 우선 적으로 가지고 와야 할 것 같은데?라는 느낌이 강하게 들어서 개발을 진행하고 돌이켜보니 &lt;span style=&quot;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;&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;DDD에 대해서 조금이라도 감을 잡기 위해서&amp;nbsp; &quot;도메인 주도 개발 시작하기 : DDD 핵심 개념 정리부터 구현까지 &amp;lt;최범균&amp;gt;&quot; 책을 구매해서 읽어보고 있는데 (현재 3장..) 내가 한건 도메인 주도 개발 이라기보다 흉내만 낸 그냥 테이블(ERD) 중심적인 개발에 가까웠던 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 도메인 책임에 대한 개념이 잘 정리가 되지 않아서 조금 더 공부를 해보고 콘서트 프로젝트를 리팩토링 하는 시간을 가져야될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://jhost.tistory.com/105&quot;&gt;2024.04.18 - [회고] - 콘서트 예약 프로젝트 회고&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713602963168&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GwazO/hyVSV8YKSq/McKKyQaNW6cSKgjICR7MwK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/kTmNy/hyVS19aj6t/Ik2B2vyekwSGIKLoyV7KK0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot; data-og-url=&quot;https://jhost.tistory.com/105&quot; data-og-source-url=&quot;https://jhost.tistory.com/105&quot; data-og-host=&quot;jhost.tistory.com&quot; data-og-description=&quot;콘서트 예약 프로젝트를 진행하며 코딩을 하기 전에 시퀀스 다이어그램, ERD, API 명세를 만들면서 이걸 내가 할 수 있을까 라는 생각이 가장 먼저 들었지만 도메인을 나눠서 그 도메인에 맞는 책&quot; data-og-title=&quot;콘서트 예약 프로젝트 회고&quot; data-og-type=&quot;article&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://jhost.tistory.com/105&quot; data-source-url=&quot;https://jhost.tistory.com/105&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GwazO/hyVSV8YKSq/McKKyQaNW6cSKgjICR7MwK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/kTmNy/hyVS19aj6t/Ik2B2vyekwSGIKLoyV7KK0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;콘서트 예약 프로젝트 회고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;콘서트 예약 프로젝트를 진행하며 코딩을 하기 전에 시퀀스 다이어그램, ERD, API 명세를 만들면서 이걸 내가 할 수 있을까 라는 생각이 가장 먼저 들었지만 도메인을 나눠서 그 도메인에 맞는 책&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;jhost.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>항해99</category>
      <category>항해플러스</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/106</guid>
      <comments>https://jhost.tistory.com/106#entry106comment</comments>
      <pubDate>Sat, 20 Apr 2024 17:48:09 +0900</pubDate>
    </item>
    <item>
      <title>콘서트 예약 프로젝트 회고</title>
      <link>https://jhost.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트 예약 프로젝트를 진행하며 코딩을 하기 전에 시퀀스 다이어그램, ERD, API 명세를 만들면서 이걸 내가 할 수 있을까 라는 생각이 가장 먼저 들었지만 도메인을 나눠서 그 도메인에 맞는 책임과 로직을 구현하다 보니 하나씩 완성할 수 있었다.&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;오늘은 고민을 많이 한 2가지 케이스에 대한 내용을 회고하려고 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 대기열 구현&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 포인트 충전 동시성 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;대기열 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열 로직을 구현하고 동시성 테스트 케이스를 2가지 작성을 하였다. (테스트 환경 : 대기열이 0명인 경우)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 한명의 사용자가 대기열 신청을 동시에 여러 번 하는 경우 한 번만 신청이 완료되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 100명의 사용자가 동시에 신청하는 경우 ONGOING 상태를 가진 사용자가 QUEUE_LIMIT(대기열 제한수)와 같아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞전에 작성한 내용으로 AtomicLong을 사용해서  대기열 카운트를 제어해서 &lt;b&gt;2번의 동시성 케이스를 통과&lt;/b&gt;를 하였지만 AtomicLong을 사용해서 처리한 경우에 분산환경에서는 의미가 없는 상태라는 피드백을 받아서 다시 수정을 하기로 했다.&lt;/p&gt;
&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 style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://jhost.tistory.com/103&quot;&gt;2024.04.10 - [프로그래밍 언어/Java] - [Java] AtomicLong으로 동시성 문제 해결 (feat. 비관적 락을 사용하지 못한 이유)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1713452974187&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Java] AtomicLong으로 동시성 문제 해결 (feat.비관적 락을 사용 하지 못한 이유)&quot; data-og-description=&quot;콘서트 예매 사이드 프로젝트를 진행하면서 대기열 관련해서 개발을 진행하면서 비관적 락을 사용해서 대기열 동시성을 풀어 나가려고 했습니다. 요구사항 중 멀티 스레드 환경에서 여러 번의&quot; data-og-host=&quot;jhost.tistory.com&quot; data-og-source-url=&quot;https://jhost.tistory.com/103&quot; data-og-url=&quot;https://jhost.tistory.com/103&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cqEoIT/hyVS5QU5ea/r12XSFS6cuAIgAvfsatW30/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/iMJWe/hyVPR7P326/VuTSmMzmU5iKSeulvyFwn0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://jhost.tistory.com/103&quot; data-source-url=&quot;https://jhost.tistory.com/103&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cqEoIT/hyVS5QU5ea/r12XSFS6cuAIgAvfsatW30/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/iMJWe/hyVPR7P326/VuTSmMzmU5iKSeulvyFwn0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;[Java] AtomicLong으로 동시성 문제 해결 (feat.비관적 락을 사용 하지 못한 이유)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;콘서트 예매 사이드 프로젝트를 진행하면서 대기열 관련해서 개발을 진행하면서 비관적 락을 사용해서 대기열 동시성을 풀어 나가려고 했습니다. 요구사항 중 멀티 스레드 환경에서 여러 번의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;jhost.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 내역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 대기열 테이블에 unique 제약조건 추가(userId_status)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 기존 대기열 상태중 WAIT, ONGOING, DONE 상태에서 DONE 상태를 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 대기열 만료시 상태를 DONE으로 update 하는 과정에서 unique 제약조건 위배 되기 때문에, 만료된 상태를 관리하는 별도의 필드를 추가 isExpired (true, false)하여 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정한 후 동시성 테스트 케이스 실행 결과 이번엔 반대로 1번은 성공하지만 2번은 실패하는 상황..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 한명의 사용자가 대기열 신청을 동시에 여러 번 하는 경우 한 번만 신청이 완료되어야 한다. (성공)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 1번의 상황은 unique제약 조건으로 인해 insert자체가 되지 않아서 당연히 성공할 거라고 예상했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 100명의 사용자가 동시에 신청하는 경우 ONGOING 상태를 가진 사용자가 QUEUE_LIMIT(대기열 제한수)와 같아야 한다. (실패)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 2번 상황이 실패하는 이유는 대기열에 ONGOING 상태인 사용자가 QUEUE_LIMIT보다 적은 경우 ONGOING상태로 진입이 되어야 하기 때문에 요청이 들어오는 경우&amp;nbsp; &lt;code&gt;SELECT COUNT(*) FORM QUEUE WHERE status = 'ONGOING'&lt;/code&gt;&amp;nbsp; ONGOING 상태의 카운트를 확인하여 비교하여 대기열 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1713450389831&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void assignQueueStatusByAvailability(Queue queue) {
    //findByStatusWithPessimisticLock -&amp;gt; 비관적 락을 걸어도 동시성 의미 없는 상태
    List&amp;lt;Queue&amp;gt; ongoingStatus = iQueueRepository.findByStatusWithPessimisticLock(WaitingStatus.ONGOING);

    if (ongoingStatus.size() &amp;lt; QUEUE_LIMIT) {
        queue.toOngoing(QUEUE_EXPIRED_TIME);
    } else {
        queue.toWait();
    }
}&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;count() 쿼리의 경우 집계 연산이기 때문에 비관적 락을 걸 수 없기 때문에, 이를 해결해 보기 위해서 위에 코드처럼 SELECT 쿼리에 락을 걸어서 시도를 해봤지만 여전히 실패..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 대기열에 있는 ROW들에 대한 LOCK은 사실상 의미가 없기 때문에 영향이 없다.&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;사실상 syncronized 키워드를 사용하거나, RDB 대신 REDIS를 사용하면 쉽게 해결할 수 있지만 이왕 시작한 거 마무리를 하고 싶은 욕심이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 해결한 문제는 아니지만 저 2가지 케이스를 동시에 성공하기 위해서는 2가지 방법이 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 대기열 관련 테이블을 2개의 테이블로 구성하여 해결하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wait_table(status - WAIT), ongoing_table(status - ONGOING, DONE)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사용자가 대기열 신청을 하는 경우 ongoing 카운트와 상관없이 무조건 WAIT_TABLE에 insert&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 대기열 만료 스케쥴러 (ongoing_table 에서 만료 시간이 지난 사용자는 DONE 상태로 업데이트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 대기열 갱신 스케쥴러 (wait_table 에서 진입한 순서대로 QUEUE_LIMIT - ONGOING count() 만큼 ONGOING 상태로 갱신)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법으로 해결할 경우 사실상 내가 고민한 동시성 고민을 할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ONGOING count의 컬럼만 존재하는 QUEUE_ONGOING_COUNT 테이블을 추가해서 ONGOING 카운트를 관리하는 방법&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;나머지 기능 구현을 완료하고 나서 아마도 위 2가지 방법 중 하나로 수정을 해야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;포인트 충전 동시성 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;point_table -&amp;gt; id, user_id, point로 간단하게 구성되어 있다.&lt;/p&gt;
&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_1713453361201&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public Point findPointByUserId(Long userId) {
    return pointJpaRepository.findByUserId(userId)
            .map(PointConverter::toDomain)
            .orElseGet(() -&amp;gt; Point.builder()
                    .userId(userId)
                    .point(0L)
                    .build());
}&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;포인트 최초 충전시 point_table에 user_id에 대한 row가 없기 때문에 0L으로 도메인을 return 해서 서비스에서 point 객체에 충전할 금액을 더해서 save를 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 포인트가 있는 유저의 충전이 다수로 들어올 경우에는 findByUserId 쿼리에서 락을 잡아서 해결하면 되지만, 첫 충전하는 경우에는 이런 상황을 어떤 식으로 풀어야 할지 도저히 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 상황이 많이 나올 것 같은데 공부를 더 해봐야 될 것 같다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용 말고도 프로젝트를 진행하면서 아직 풀지 못한 내용이 많은데 하나씩 내용을 정리하는 시간을 가져야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>대기열 테스트</category>
      <category>동시성</category>
      <category>콘서트 사이드 프로젝트</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/105</guid>
      <comments>https://jhost.tistory.com/105#entry105comment</comments>
      <pubDate>Thu, 18 Apr 2024 23:53:23 +0900</pubDate>
    </item>
    <item>
      <title>항해 플러스 4주차 회고</title>
      <link>https://jhost.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4주차 과제를 진행하면서 생긴 고민&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트 예약 프로젝트를 선정하고, 대기열 시스템을 구현하며 동시성 테스트를 작성했는데 통과하지 못하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열 구현을 잘못한 것 같아서 지금 어떻게 해결해야 할지 고민하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 테스트 시나리오는 2가지로 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 여러 명의 사용자가 동시에 신청하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; QUEUE_LIMIT 가 50인 경우, 100명의 사용자가 신청이 들어와도 50명의 사용자만 ONGOING 상태로 들어와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 한 명의 사용자가 여러 번 신청하는 경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; QUEUE 테이블에 한 번만 들어와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 여러 명의 사용자가 동시에 신청하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시나리오는 ONGOING 상태의 카운트를 QueueService가 init 되는 시점에 OngoingCount를 가지고 와서 QueueOption 도메인에 AtomicLong으로 카운트를 관리하는 식으로 해결했지만, 멘토링 결과 AtomicLong으로 해결하는 것은 동시성은 통과되어도 분산 환경에서 적절하지 않은 것이라고 판단.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 한 명의 사용자가 여러 번 신청하는 경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 해결하지 못한 문제로 갈피를 못 잡고 있는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열을 로직을 우선적으로 다시 구현을 해야 할지 다른 로직부터 먼저 구현하고 진행을 해야 할지 고민이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 시나리오들을 해결하기 위해서 분산 환경 및 동시성 제어 키워드로 조금 더 공부를 해야할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 엔티티와 도메인을 분리해서 사용하는 경우 infra에서 조회했을 때 값이 없는 상태인 경우..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;infra 계층에서 엔티티 조회 시 값이 없는 경우에 infra에서 EntitiyNotFoundException을 throw를 해야 하나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service계층에서 throw를 하는 게 맞을 것 같다라고 고민을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멘토링 결과 : Infra에서 값이 없는 경우에 null로 service에서 return -&amp;gt; service에서 null check 하는 방식으로 권장하시는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 주를 마무리하며&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주에 대기열 도메인을 해결하기 위해 다양한 케이스와 예외사항을 생각하면서 정말 많은 시나리오를 구상했지만 결국은 FAIL..&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;위에서 시나리오를 통과하기 위해서 대기열 테이블에 대한 row를 insert를 하지 않고, 하나의 row로 update 하면서 userId에 대한 unique key를 만들어서 해결하는 시나리오로 고민을 해보고 재구성을 해봐야 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 주차까지 까지 완벽한 구현을 위해서 파이팅!&lt;/p&gt;</description>
      <category>회고</category>
      <category>항해99</category>
      <category>항해플러스</category>
      <author>:j</author>
      <guid isPermaLink="true">https://jhost.tistory.com/104</guid>
      <comments>https://jhost.tistory.com/104#entry104comment</comments>
      <pubDate>Sat, 13 Apr 2024 16:25:37 +0900</pubDate>
    </item>
  </channel>
</rss>