<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>과거의 나를 위해</title>
    <link>https://pinggoopark.tistory.com/</link>
    <description>데이터엔지니어 / Data Engineer</description>
    <language>ko</language>
    <pubDate>Sat, 11 Apr 2026 02:26:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>박경태</managingEditor>
    <image>
      <title>과거의 나를 위해</title>
      <url>https://tistory1.daumcdn.net/tistory/4212366/attach/35b642ed674344318d99f63b37d8805f</url>
      <link>https://pinggoopark.tistory.com</link>
    </image>
    <item>
      <title>안양웨딩홀 더파티움 안양 계약후기</title>
      <link>https://pinggoopark.tistory.com/entry/%EC%95%88%EC%96%91%EC%9B%A8%EB%94%A9%ED%99%80-%EB%8D%94%ED%8C%8C%ED%8B%B0%EC%9B%80-%EC%95%88%EC%96%91-%EA%B3%84%EC%95%BD%ED%9B%84%EA%B8%B0</link>
      <description>&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 드디어 웨딩홀 계약을 마친 예비신랑입니다 ㅎㅎ 결혼 준비하면서 웨딩홀 선택이 제일 중요하다는 얘기를 많이 들었는데, 정말 맞는 말이더라고요. 저희도 안양 지역으로 거의 확정하고 여러 곳을 돌아다녔는데 생각보다 고민이 많았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 '그냥 괜찮아 보이는 데 가면 되지 않나?' 싶었는데, 막상 발품 팔면서 보니까 각각 미묘한 차이들이 있더라구요. 홀은 예쁜데 주차가 안 되거나, 주차는 되는데 위치가 애매하거나... 완벽한 곳을 찾기가 생각보다 쉽지 않았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;kakaotv&quot; data-video-url=&quot;https://tv.kakao.com/v/460828272&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/zkoWf/dJMb9cBCkcl/ztR2mkI8oaZa3rE0MU8QxK/img.jpg?width=806&amp;amp;height=708&amp;amp;face=338_35_416_119,https://scrap.kakaocdn.net/dn/ur1NC/dJMb9frzErT/8sJ8ov9wwOtjPtgskP8te0/img.jpg?width=806&amp;amp;height=708&amp;amp;face=338_35_416_119&quot; data-video-width=&quot;806&quot; data-video-height=&quot;708&quot; data-video-origin-width=&quot;806&quot; data-video-origin-height=&quot;708&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;'과거의 나를 위해'에서 업로드한 동영상&quot; data-video-play-service=&quot;daum_tistory&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://play-tv.kakao.com/embed/player/cliplink/460828272?service=daum_tistory&quot; width=&quot;806&quot; height=&quot;708&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안양 웨딩홀 투어 시작!&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 처음엔 인터넷으로만 보고 한두 군데만 가볼까 했는데, 와이프가 &quot;그래도 직접 봐야지&quot;라면서 주말 하루 잡아서 안양 웨딩홀들을 쭉 돌아다녔어요. 아침 일찍 출발해서 저녁까지 정말 열심히 다녔던 것 같아요 ㅋㅋ&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;A웨딩홀 - 지하철역 가깝지만 주차가...&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 간 곳이 A웨딩홀이었는데요, 여기는 일단 지하철역에서 진짜 가까웠어요. 도보로 5분도 안 걸리더라고요. 대중교통 이용하시는 하객분들한테는 정말 좋을 것 같았어요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는 주차장이었어요. 주차장이 한 층밖에 없는데다가 생각보다 규모가 크지 않더라고요. 결혼식 날 하객분들이 주차 때문에 헤매시는 건 너무 죄송할 것 같아서 고민이 됐어요. 특히 저희는 지방에서 오시는 분들도 많고, 연세 있으신 분들도 계셔서 주차는 정말 중요한 포인트였거든요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홀 자체는 깔끔하고 괜찮았는데, 이 부분 때문에 일단 보류하기로 했어요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;B웨딩홀 - 시설은 좋은데 위치가 애매해요&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 간 곳은 B웨딩홀이었어요. 여기는 시설이 정말 좋더라고요. 리모델링을 최근에 한 건지 전체적으로 엄청 깨끗하고 모던한 느낌이었어요. 홀도 넓고, 음식 평도 괜찮다고 들었고요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는 위치였어요. 저희 친척분들이 대부분 지방에 계시는데, 서울 오실 때 4호선을 주로 이용하시거든요. 그런데 B웨딩홀은 4호선역에서 좀 멀었어요. 버스를 타거나 택시를 타야 하는 거리라서... 특히 어르신들이 길 찾기 어려우실 것 같더라고요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와이프랑 &quot;우리야 차로 가면 되지만 하객분들 생각하면...&quot; 하면서 이것도 조금 고민스러웠어요. 시설은 진짜 좋았는데 접근성 때문에 아쉬웠던 곳이에요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C웨딩홀 - 아기자기한데 주차폭이 좁아요&lt;/h3&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째로 간 C웨딩홀은 분위기가 정말 아기자기하고 예뻤어요! 소규모 웨딩홀 느낌이랄까? 인테리어도 화이트톤으로 깔끔하게 되어있고, 사진 찍으면 진짜 예쁘게 나올 것 같더라고요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기도 주차장이 문제였어요. 주차장이 있긴 한데 주차폭이 너무 좁은 거예요. 저희가 갔을 때도 옆 차 조심하면서 조심조심 주차했는데, 운전 초보이신 분들이나 큰 차 가지고 오신 분들은 주차하기 진짜 힘들겠더라고요.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와이프가 &quot;결혼식 날 주차하다가 차 긁으면 기분 안 좋을 것 같은데...&quot;라고 해서 이것도 패스하게 됐어요. 홀은 정말 마음에 들었는데 아쉬웠던 곳이에요 ㅠㅠ&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더파티움 안양 발견!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 세 곳을 돌아보고 나니까 점심시간이 훌쩍 지나있더라고요. 배고프고 지쳐서 &quot;오늘은 이 정도만 보고 다음에 또 보자&quot;고 했는데, 와이프가 &quot;여기까지 온 김에 한 군데만 더 보고 가자&quot;면서 더파티움 안양을 검색하더라구요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 안양웨딩홀 더파티움 안양은 저도 이름은 들어봤어요. 몇 달 전에 사촌형 결혼식이 여기였거든요! 그때 하객으로 참석했을 때 '여기 괜찮네?' 싶었는데, 막상 우리 결혼식장으로 생각하니까 또 다르게 보이더라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 들어가자마자 느낌이 확 달랐어요. 일단 주차장부터가 많은 거예요! 주차하는데 전혀 불편함이 없었고, 층도 여러 층이라서 자리도 많아 보였어요. 첫인상부터 &quot;오... 여기 괜찮은데?&quot; 했어요 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직원분이 친절하게 안내해주셨는데, 더파티움 안양은 5층 브리에홀과 연회장, 6층 연회장, 7층 라포레홀로 구성되어 있다고 하시더라고요. 층마다 홀이 있어서 여러 타임에 결혼식이 진행되는 구조였어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsdNo0/dJMcajnrdGq/Tvj52SS7MGehoI1BxrRfn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsdNo0/dJMcajnrdGq/Tvj52SS7MGehoI1BxrRfn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsdNo0/dJMcajnrdGq/Tvj52SS7MGehoI1BxrRfn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsdNo0%2FdJMcajnrdGq%2FTvj52SS7MGehoI1BxrRfn1%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;216&quot; height=&quot;200&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjGyRy/dJMcahpGi5s/19IBaRkhIUpvpnFQ5ZJMM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjGyRy/dJMcahpGi5s/19IBaRkhIUpvpnFQ5ZJMM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjGyRy/dJMcahpGi5s/19IBaRkhIUpvpnFQ5ZJMM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjGyRy%2FdJMcahpGi5s%2F19IBaRkhIUpvpnFQ5ZJMM0%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;188&quot; height=&quot;251&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 갔을 때 기준으로는 예식 시간이 여러 타임 있었는데요,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5층 브리에홀은 11:30, 12:50, 14:10, 15:30, 16:50, 18:10&lt;/li&gt;
&lt;li&gt;7층 라포레홀은 11:00, 12:20, 13:40, 15:00&lt;/li&gt;
&lt;/ul&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;645&quot; data-origin-height=&quot;865&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDp0W/dJMcac9GlPg/c2u9EhiumYt6y0qciDKTZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDp0W/dJMcac9GlPg/c2u9EhiumYt6y0qciDKTZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDp0W/dJMcac9GlPg/c2u9EhiumYt6y0qciDKTZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDp0W%2FdJMcac9GlPg%2Fc2u9EhiumYt6y0qciDKTZ0%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;175&quot; height=&quot;235&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;865&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCDKbt/dJMcahQJ1bj/DrFfPDIfrs9tK68ChKOtLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCDKbt/dJMcahQJ1bj/DrFfPDIfrs9tK68ChKOtLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCDKbt/dJMcahQJ1bj/DrFfPDIfrs9tK68ChKOtLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCDKbt%2FdJMcahQJ1bj%2FDrFfPDIfrs9tK68ChKOtLK%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;187&quot; height=&quot;243&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더파티움 안양 구석구석 탐방기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫인상부터 다른 디테일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘리베이터 타고 올라갔는데 벽면이며 복도며 곳곳에 꽃 장식이 정말 많더라고요. 그냥 조화 몇 개 걸어둔 게 아니라 진짜 신경 써서 꾸며놨다는 게 느껴졌어요. 색감도 은은하고 세련되게 매치되어 있고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와이프랑 &quot;여기 인테리어 담당자가 센스가 좋나봐&quot;라고 얘기했어요 ㅋㅋ 웨딩홀마다 분위기가 다 다르잖아요. 어떤 곳은 너무 화려하고, 어떤 곳은 너무 심플하고... 근데 여기는 딱 적당히 화려하면서도 촌스럽지 않은 느낌? 사진 찍어도 예쁘게 나올 것 같은 분위기였어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 층고가 높아서 개방감이 진짜 좋았어요! 답답한 느낌이 전혀 없었어요. 여기는 쭉 뻥 뚫린 느낌이라서 정말 시원시원했어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;852&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq5svE/dJMcabXh7Q0/c1zkGvevnVbURVeWSdUkqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq5svE/dJMcabXh7Q0/c1zkGvevnVbURVeWSdUkqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq5svE/dJMcabXh7Q0/c1zkGvevnVbURVeWSdUkqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq5svE%2FdJMcabXh7Q0%2Fc1zkGvevnVbURVeWSdUkqk%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;227&quot; height=&quot;303&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;852&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bulmPC/dJMb99ZtYvI/IKSPRVxZN8zu9jc4aj7ie0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bulmPC/dJMb99ZtYvI/IKSPRVxZN8zu9jc4aj7ie0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bulmPC/dJMb99ZtYvI/IKSPRVxZN8zu9jc4aj7ie0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbulmPC%2FdJMb99ZtYvI%2FIKSPRVxZN8zu9jc4aj7ie0%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;217&quot; height=&quot;285&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/85AV4/dJMb99ZtYv4/lsyin8OxdAEwOxtafehuQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/85AV4/dJMb99ZtYv4/lsyin8OxdAEwOxtafehuQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/85AV4/dJMb99ZtYv4/lsyin8OxdAEwOxtafehuQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F85AV4%2FdJMb99ZtYv4%2Flsyin8OxdAEwOxtafehuQ0%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;204&quot; height=&quot;272&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;846&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;h3 data-ke-size=&quot;size23&quot;&gt;7층 라포레홀 - 웅장함의 끝판왕&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7층 라포레홀도 구경했는데 진짜 입이 떡 벌어지더라고요. 천장에 대형 스크린이 있는데 그게 엄청 크고 웅장해 보였어요. 영화관 스크린처럼 크다고 해야 하나? 입장 영상이나 사진 영상 틀면 진짜 감동적일 것 같았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포토테이블 쪽도 꽃 장식이 정말 예뻤어요. 저희가 갔을 때는 화이트와 연핑크 톤으로 로맨틱하게 꾸며져 있더라고요. 사진 찍으면 진짜 인스타감성 제대로 날 것 같았어요!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdo2vf/dJMcaiB4ObN/Jr56mzTKopm3VcgeALEZKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdo2vf/dJMcaiB4ObN/Jr56mzTKopm3VcgeALEZKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdo2vf/dJMcaiB4ObN/Jr56mzTKopm3VcgeALEZKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdo2vf%2FdJMcaiB4ObN%2FJr56mzTKopm3VcgeALEZKk%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;199&quot; height=&quot;251&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;835&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drMaE8/dJMcaaYnWJS/LSjhGLyWnYdlLOQG3dkDkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drMaE8/dJMcaaYnWJS/LSjhGLyWnYdlLOQG3dkDkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drMaE8/dJMcaaYnWJS/LSjhGLyWnYdlLOQG3dkDkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrMaE8%2FdJMcaaYnWJS%2FLSjhGLyWnYdlLOQG3dkDkK%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;199&quot; height=&quot;254&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfCqSK/dJMcai28fxU/0zHyhCEQy10092IuB5NsU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfCqSK/dJMcai28fxU/0zHyhCEQy10092IuB5NsU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfCqSK/dJMcai28fxU/0zHyhCEQy10092IuB5NsU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfCqSK%2FdJMcai28fxU%2F0zHyhCEQy10092IuB5NsU0%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;210&quot; height=&quot;277&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t0vkV/dJMcadU3Tnr/fXNLGvQH8b61guJoBpt4B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t0vkV/dJMcadU3Tnr/fXNLGvQH8b61guJoBpt4B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t0vkV/dJMcadU3Tnr/fXNLGvQH8b61guJoBpt4B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft0vkV%2FdJMcadU3Tnr%2FfXNLGvQH8b61guJoBpt4B1%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;208&quot; height=&quot;277&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&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;5층이랑 6층 연회장도 쭉 둘러봤는데요, 진짜 넓었어요. 테이블 간격도 여유롭게 배치되어 있어서 하객분들이 편하게 식사하실 수 있을 것 같더라고요. 어떤 웨딩홀은 테이블이 너무 빽빽해서 지나다니기도 불편한 곳도 있었거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 뷔페 음식들이 정말 맛있어 보였어요! 마침 다른 결혼식이 진행 중이어서 음식들이 나와 있었는데, 비주얼부터가 달랐어요. 신선해 보이고 정성스럽게 준비된 느낌?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사촌형 결혼식 때 여기서 먹었던 음식이 맛있었던 게 생각나더라고요. 특히 초밥이랑 빵이 진짜 괜찮았거든요. 요즘 웨딩홀 음식 별로라는 말 많이 들었는데, 여기는 하객분들이 만족하실 것 같았어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ur1wy/dJMcagK42kc/KZqcBbDoKSHorVmURKM3k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ur1wy/dJMcagK42kc/KZqcBbDoKSHorVmURKM3k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ur1wy/dJMcagK42kc/KZqcBbDoKSHorVmURKM3k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fur1wy%2FdJMcagK42kc%2FKZqcBbDoKSHorVmURKM3k0%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;232&quot; height=&quot;309&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&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;h3 data-ke-size=&quot;size23&quot;&gt;신부대기실 - 넓고 여유로워요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5층 신부대기실도 구경했는데 정말 깊고 넓었어요! 친구들이랑 가족들 오셔도 충분히 다 들어갈 수 있을 것 같고, 사진 찍을 공간도 넉넉해 보였어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조명도 밝고 의자도 여러 개 있어서 하객 맞이하다가 쉬기도 편할 것 같고요. 어떤 대기실은 진짜 좁아서 사람 몇 명만 들어가도 꽉 차던데, 여기는 그런 걱정은 전혀 없을 것 같았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화장실도 대기실 바로 옆에 있어서 편하겠더라고요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qEu66/dJMcagxwh5P/xKq0ypUxCkXtMkzKmqYE4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qEu66/dJMcagxwh5P/xKq0ypUxCkXtMkzKmqYE4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qEu66/dJMcagxwh5P/xKq0ypUxCkXtMkzKmqYE4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqEu66%2FdJMcagxwh5P%2FxKq0ypUxCkXtMkzKmqYE4k%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;207&quot; height=&quot;276&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2784&quot; data-origin-height=&quot;3712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BkZbB/dJMcacWbUBP/BHncSWkxKPXWkZeAlMED81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BkZbB/dJMcacWbUBP/BHncSWkxKPXWkZeAlMED81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BkZbB/dJMcacWbUBP/BHncSWkxKPXWkZeAlMED81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBkZbB%2FdJMcacWbUBP%2FBHncSWkxKPXWkZeAlMED81%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;212&quot; height=&quot;283&quot; data-origin-width=&quot;2784&quot; data-origin-height=&quot;3712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZ3LoT/dJMcacIF62i/h2FzXiODiNKIO7k4h94gsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZ3LoT/dJMcacIF62i/h2FzXiODiNKIO7k4h94gsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZ3LoT/dJMcacIF62i/h2FzXiODiNKIO7k4h94gsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZ3LoT%2FdJMcacIF62i%2Fh2FzXiODiNKIO7k4h94gsk%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;219&quot; height=&quot;292&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리가 계약한 5층 브리에홀!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7층도 좋았지만 저희는 5층 브리에홀이 더 마음에 들었어요. 일단 규모가 저희한테 딱 맞았어요. 너무 크지도 작지도 않은 사이즈?&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ca0K3R/dJMcahJZgW1/uEtprimcpK96KviH2ftuZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ca0K3R/dJMcahJZgW1/uEtprimcpK96KviH2ftuZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ca0K3R/dJMcahJZgW1/uEtprimcpK96KviH2ftuZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fca0K3R%2FdJMcahJZgW1%2FuEtprimcpK96KviH2ftuZk%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;189&quot; height=&quot;252&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ncav0/dJMcafFoya6/cpWvil1KiGD7YKQa1d5ES1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ncav0/dJMcafFoya6/cpWvil1KiGD7YKQa1d5ES1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ncav0/dJMcafFoya6/cpWvil1KiGD7YKQa1d5ES1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fncav0%2FdJMcafFoya6%2FcpWvil1KiGD7YKQa1d5ES1%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;196&quot; height=&quot;261&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2768&quot; data-origin-height=&quot;3690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8PZss/dJMcagqKENq/kHqVDaw051MWtOOSrH8UuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8PZss/dJMcagqKENq/kHqVDaw051MWtOOSrH8UuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8PZss/dJMcagqKENq/kHqVDaw051MWtOOSrH8UuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8PZss%2FdJMcagqKENq%2FkHqVDaw051MWtOOSrH8UuK%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;197&quot; height=&quot;263&quot; data-origin-width=&quot;2768&quot; data-origin-height=&quot;3690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bezvHc/dJMcagqKENC/eHi5qeIImomnce8vBydeXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bezvHc/dJMcagqKENC/eHi5qeIImomnce8vBydeXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bezvHc/dJMcagqKENC/eHi5qeIImomnce8vBydeXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbezvHc%2FdJMcagqKENC%2FeHi5qeIImomnce8vBydeXK%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;204&quot; height=&quot;272&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2777&quot; data-origin-height=&quot;3703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIyOLL/dJMcadOg7qA/Uv3TO8TEgKqE5jZC6JjG01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIyOLL/dJMcadOg7qA/Uv3TO8TEgKqE5jZC6JjG01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIyOLL/dJMcadOg7qA/Uv3TO8TEgKqE5jZC6JjG01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIyOLL%2FdJMcadOg7qA%2FUv3TO8TEgKqE5jZC6JjG01%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;211&quot; height=&quot;281&quot; data-origin-width=&quot;2777&quot; data-origin-height=&quot;3703&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예비신랑신부님들께 추천해요!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 솔직하게 말씀드리면, 더파티움 안양 정말 추천해요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주차 걱정 완전 제로예요.&lt;/b&gt; 주차장이 많고 층도 여러 층이라서 자리 걱정 없어요. 특히 지방에서 차 타고 오시는 분들 많으시면 이게 진짜 중요하거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;홀이 정말 예뻐요.&lt;/b&gt; 사진 찍으면 다 예쁘게 나올 것 같아요. 인테리어도 세련되고, 조명도 좋고, 전체적인 분위기가 고급스러워요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;음식이 맛있어요.&lt;/b&gt; 제가 직접 먹어봤으니까 자신 있게 말씀드려요! 하객분들 만족도 높을 거예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위치도 괜찮아요.&lt;/b&gt; 4호선 안양역에서 가깝고, 자차로 오셔도 주차 편하고, 접근성이 좋아요.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 지도에서 '더파티움 안양' 검색하시면 바로 나와요!&lt;/li&gt;
&lt;li&gt;카카오맵에서도 찾으실 수 있어요.&lt;/li&gt;
&lt;li&gt;주소는 직접 검색해보시는 게 정확해요~&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 지도: &lt;a href=&quot;https://naver.me/IFgdHqLz&quot;&gt;https://naver.me/IFgdHqLz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;834&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQHdUN/dJMcab33iNR/ABdsDMkuyKOtYK8QX5b9UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQHdUN/dJMcab33iNR/ABdsDMkuyKOtYK8QX5b9UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQHdUN/dJMcab33iNR/ABdsDMkuyKOtYK8QX5b9UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQHdUN%2FdJMcab33iNR%2FABdsDMkuyKOtYK8QX5b9UK%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;1448&quot; height=&quot;834&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;834&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 지도: &lt;a href=&quot;https://kko.to/COPviQxCq5&quot;&gt;https://kko.to/COPviQxCq5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxsVfk/dJMcaaYnWHb/kHgocK8Fdl0EB53kbJbv3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxsVfk/dJMcaaYnWHb/kHgocK8Fdl0EB53kbJbv3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxsVfk/dJMcaaYnWHb/kHgocK8Fdl0EB53kbJbv3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxsVfk%2FdJMcaaYnWHb%2FkHgocK8Fdl0EB53kbJbv3k%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;1477&quot; height=&quot;821&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;821&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;</description>
      <category>개인 일정/일상</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1110</guid>
      <comments>https://pinggoopark.tistory.com/entry/%EC%95%88%EC%96%91%EC%9B%A8%EB%94%A9%ED%99%80-%EB%8D%94%ED%8C%8C%ED%8B%B0%EC%9B%80-%EC%95%88%EC%96%91-%EA%B3%84%EC%95%BD%ED%9B%84%EA%B8%B0#entry1110comment</comments>
      <pubDate>Fri, 23 Jan 2026 15:04:59 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.jav 분석</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-zeppelin-zenginesrcmainjavaorgapachezeppelinnotebookrepoVFSNotebookRepojav-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스의 역할 ― &amp;ldquo;노트북 파일을 실제 파일시스템(또는 원격)에 안전하게 저장&amp;middot;조회&amp;middot;이동&amp;middot;삭제&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;구현 인터페이스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AbstractNotebookRepo (Zeppelin 공통 저장소 추상 클래스)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;핵심 책임&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;① 저장(save) ② 조회(get) ③ 목록(list) ④ 이동(move) ⑤ 삭제(remove) ⑥ 설정 조회/갱신&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주요 기술&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Apache VFS(Virtual File System) &amp;mdash; 로컬&amp;middot;HDFS&amp;middot;S3&amp;middot;FTP 등 다양한 스킴을 하나의 API로 다룸&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;왜 VFS인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin 노트북은 로컬 디스크뿐만 아니라 S3, HDFS, WebDAV 등으로도 저장될 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Apache VFS는 스킴별 구현체(FileProvider)만 설정하면 동일한 코드로 모든 스토리지를 다룰 수 있는 추상화 계층을 제공함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필드 살펴보기&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;protected FileSystemManager fsManager;          // VFS 진입점
protected FileObject rootNotebookFileObject;    // 노트북 루트 디렉터리
protected String rootNotebookFolder;            // 위 디렉터리의 URI 문자열 (Windows 호환 처리 포함)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;FileSystemManager는 VFS의 connection pool이자 resolveFile() 팩토리.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;FileObject는 VFS가 추상화한 &amp;ldquo;파일 또는 폴더&amp;rdquo; 핸들.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;rootNotebookFolder를 문자열로 별도 보관하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Windows에서 getPath()가 드라이브 레터를 잃어버리는 버그 방지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;-매 list() 호출 때마다 fresh FileObject를 다시 만들어 최신 파일 상태 반영.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. init()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void init(ZeppelinConfiguration zConf, NoteParser noteParser)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;부모 AbstractNotebookRepo.init() &amp;rarr; 공용 설정/파서 세팅.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;노트북 디렉터리 경로를 설정 (setNotebookDirectory()).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. setNotebookDirectory()&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 zeppelin-site.xml 에 설정한 ZEPPELIN_NOTEBOOK_DIR 값을 로그로 출력.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Windows 경로 여부 판단 &amp;rarr; 로컬 File &amp;rarr; toURI(), 아니면 new URI() 직접 생성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;URI에 스킴이 없으면 로컬 경로로 간주하고 zConf.getAbsoluteDir()로 절대경로 보정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;VFS.getManager()로 FileSystemManager 획득.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;fsManager.resolveFile(filesystemRoot) &amp;rarr; rootNotebookFileObject 생성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;폴더가 없으면 createFolder()로 자동 생성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;rootNotebookFolder 문자열 캐시 (Windows는 &quot;file:///&quot; &amp;rarr; &quot;/&quot; 치환).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;윈도우 특수 처리가 핵심임.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;file:///C:/path 같은 3슬래시 URI가 VFS windows provider에서 문제를 일으켜 &quot;/C:/path&quot; 형태로 정규화함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;노트 목록 조회&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public Map&amp;lt;String, NoteInfo&amp;gt; list(AuthenticationInfo subject)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;매 호출마다 rootNotebookFileObject = fsManager.resolveFile(rootNotebookFolder)로 새 FileObject 취득&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rarr; 캐시로 인해 디렉터리 변경 사항을 놓치는 문제 방지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실질 로직은 재귀 메서드 listFolder(FileObject)가 수행.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;listFolder()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 숨김파일(. 시작) 무시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 폴더라면 자식들에게 재귀.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 파일이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 확장자가 .zpln인지 검사.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;-getNoteId()로 UUID 추출 ({title}_{id}.zpln 규칙).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;-getNotePath()로 &amp;ldquo;UI에 표시될 경로&amp;rdquo; 생성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;-NoteInfo(noteId, notePath) put.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;노트 로드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public Note get(String noteId, String notePath, AuthenticationInfo subject)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;rootNotebookFileObject.resolveFile(buildNoteFileName(noteId, notePath))&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- NameScope.DESCENDENT 지정 &amp;rarr; 루트 이하 어디든 탐색 허용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;파일 내용을 IOUtils.toString()으로 읽어 JSON &amp;rarr; noteParser.fromJson().&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예외 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- CorruptedNoteException 발생 시 파일 경로와 content를 포함한 상세 메시지로 감싸 재throw.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;노트 저장(쓰기)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public synchronized void save(Note note, AuthenticationInfo subject)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;동기화(synchronized)로 동시 저장 충돌 방지.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;임시 파일(buildNoteTempFileName)에 먼저 작성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- getContent().getOutputStream(false) &amp;rarr; &amp;ldquo;덮어쓰기&amp;rdquo; 모드.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;성공하면 moveTo()로 최종 이름(buildNoteFileName)으로 원자적 rename.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 대부분의 파일시스템에서 같은 디렉터리 내 rename은 atomic &amp;rarr; 전력 손실/프로세스 중단에도 깨지지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;노트&lt;/b&gt;: move(String noteId, String notePath, String newNotePath, ...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;폴더&lt;/b&gt;: move(String folderPath, String newFolderPath, ...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;공통 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- 원본/대상 FileObject 생성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- destFileObject.getParent().createFolder()로 부모 폴더 선제 생성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- fileObject.moveTo(destFileObject) &amp;rarr; VFS 내부적으로 copy + delete or rename 중 최적 경로.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;노트&lt;/b&gt;:noteFile.delete(Selectors.SELECT_SELF)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;폴더&lt;/b&gt;:folderObject.deleteAll() &amp;rarr; 재귀적 삭제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;저장소 설정 노출&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public List&amp;lt;NotebookRepoSettingsInfo&amp;gt; getSettings(AuthenticationInfo subject)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin UI &amp;rarr; &amp;ldquo;Storage Settings&amp;rdquo; 탭에 나타날 메타데이터.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;type=INPUT 이므로 텍스트 박스로 경로 수정 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;업데이트&lt;/b&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void updateSettings(Map&amp;lt;String, String&amp;gt; settings, AuthenticationInfo subject)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;빈 맵 방어 로깅.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Notebook Path&quot; 키 존재 여부 확인.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;공백 방어 후 setNotebookDirectory() 재호출 &amp;rarr; 런타임 중 경로 전환 지원.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;동시성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;save()는 synchronized, 이외 메서드는 동시 접근 전제(immutable FileSystemManager)로 안전.다만 updateSettings() 중 디렉터리 변경과 다른 스레드 접근이 겹치면 race condition 가능성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;성능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;매 list()에서 전체 트리 재귀 &amp;rarr; 노트 수천 개 이상이면 지연 커질 수 있음.캐싱 전략(ETag, timestamp)을 추가하면 개선 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;스킴 지원&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;VFS Provider만 클래스패스에 넣으면 s3://, hdfs:// 등 바로 동작.(단, Kerberos&amp;middot;AWS 자격증명 처리는 별도 구성 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;보안&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재 클래스는 노트 경로와 인증 정보(AuthenticationInfo) 사이의 ACL 검증을 하지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;상위 계층(ZeppelinServer, NoteManager)이 권한 체크 후 repo에 위임하는 구조임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로컬 temp 디렉터리에 VFSNotebookRepo를 초기화하고 CRUD 시나리오를 돌리는 integration test가 일반적.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;VFSNotebookRepo는 Zeppelin 노트 데이터를 플러그형 파일시스템 추상화인 Apache VFS 위에 올려, 운영 환경마다 다른 스토리지를 한 가지 코드로 지원함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주요 포인트는 윈도우&amp;middot;리눅스 호환 경로 처리, 원자적 임시 파일 저장, 매 호출 fresh FileObject 확보이며, 동시성/성능/보안 레이어는 상위 모듈과 협조하여 완성됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 구조를 이해하면 로컬 외 S3&amp;middot;HDFS 백엔드를 손쉽게 끼워 넣거나, 대용량 환경에서 캐싱/ACL/버전 관리 기능을 확장할 수 있음.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1109</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-zeppelin-zenginesrcmainjavaorgapachezeppelinnotebookrepoVFSNotebookRepojav-%EB%B6%84%EC%84%9D#entry1109comment</comments>
      <pubDate>Sat, 21 Jun 2025 13:49:20 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] zeppelin/zeppelin-web/src/components/websocket/websocket-event.factory.js 분석</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-zeppelinzeppelin-websrccomponentswebsocketwebsocket-eventfactoryjs-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;팩토리 선언부: 싱글턴 WebSocket 게이트웨이&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;angular.module('zeppelinWebApp')
       .factory('websocketEvents', WebsocketEventFactory);
function WebsocketEventFactory($rootScope, $websocket, $location,
                               baseUrlSrv, saveAsService, ngToast) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Angular 팩토리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DI 컨테이너에 싱글턴을 등록. 한 번 생성되면 앱 생애주기 동안 동일 인스턴스를 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ngInject&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ES-build 단계에서 주입 대상 이름을 주석으로 보존해 minify safe 한 의존성 주입 보장&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주입 모듈&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;$websocket(angular-websocket 래퍼), $rootScope(애플리케이션 전역 이벤트 버스), $location(SPA 라우팅), baseUrlSrv(Zeppelin 서버 URL 헬퍼) 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;websocketCalls&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;팩토리가 반환할 퍼사드(메서드 모음) 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;uniqueClientId&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;페이지 로드 시 Math.random()으로 생성하는 5-byte base-36 ID.서버와 요청&amp;ndash;응답 매칭&amp;middot;동시 편집 충돌 방지용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;lastMsgIdSeqSent&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;같은 세션 내에서 증분되는 시퀀스 값&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;pingIntervalId&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;10 초 heartbeat 핑 타이머 ID&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;변수 역할&lt;/b&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;websocketCalls.ws = $websocket(baseUrlSrv.getWebsocketUrl());
websocketCalls.ws.reconnectIfNotNormalClose = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자동 재연결: 비정상 종료(코드 1006 등)일 때만 재연결 시도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결 수립 (onOpen) &amp;amp; Heartbeat&amp;nbsp; &amp;nbsp;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;websocketCalls.ws.onOpen(() =&amp;gt; {
  $rootScope.$broadcast('setConnectedStatus', true);
  pingIntervalId = setInterval(() =&amp;gt; {
    websocketCalls.sendNewEvent({ op: 'PING' });
  }, 10000);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;연결되면 전역 UI(상단 상태 LED 등)에 &amp;ldquo;Connected&amp;rdquo; 상태 브로드캐스트.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;10 초 간격으로 서버 PING 전송 &amp;rarr; 아이들 타임아웃 방지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클라이언트 -&amp;gt; 서버 송신&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;websocketCalls.sendNewEvent = function (data) {
  // ① 인증 메타데이터 주입
  if ($rootScope.ticket) { ... }

  // ② 메시지 ID 부여: &amp;lt;clientId&amp;gt;-&amp;lt;seq&amp;gt;
  data.msgId = uniqueClientId + '-' + (++lastMsgIdSeqSent);

  // ③ 직렬화 &amp;amp; send
  return websocketCalls.ws.send(JSON.stringify(data));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin REST 로그인 시 발급된 ticket/principal/roles 를 헤더처럼 동봉.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메시지-ID 규약: abc12-7 &amp;rarr; 첫 부분은 클라이언트 랜덤 ID, 두 번째는 단조 증가 시퀀스.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 -&amp;gt; 클라이언트 수신 핸들러&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-1. 메시지 전처리&lt;/b&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;websocketCalls.ws.onMessage(event =&amp;gt; {
  const payload = angular.fromJson(event.data);
  const { op, data, msgId } = payload;
  const uniqueIdFromMsg = msgId?.split('-')[0];
  const seqFromMsg = msgId ? parseInt(msgId.split('-')[1]) : undefined;

  // 클라이언트 자신이 방금 보낸 요청에 대한 응답인지 판별
  const isResponseFromSameClient = uniqueIdFromMsg === uniqueClientId;
  ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;잠재적 버그&lt;/b&gt;&lt;br /&gt;내부 const uniqueClientId = msgId ? ... 로 이름이 겹쳐 바깥쪽 상수를 가리는 섀도잉이 발생.&lt;br /&gt;의도상 uniqueIdFromMsg 같은 다른 변수명을 써야 안전함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-2. op 별 라우팅 (거대한 if-else)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;서버에서 내려오는 op(opcode) 값을 switch-case 대신 if-else 체인으로 분기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대응 액션은 대개 $rootScope.$broadcast(...) &amp;rarr; 전역 스코프가 여러 자식 컨트롤러로 이벤트 뿌림.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;대표 예시&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;NOTE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;노트 전체 본문을 setNoteContent 이벤트로 전파&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;PARAGRAPH&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단일 paragraph 업데이트.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;짧은 지름길(optimistic UI) 방지 로직 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ERROR_INFO&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;BootstrapDialog 모달 노출, 모든 모달 일괄 닫기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;INTERPRETER_INSTALL_RESULT, NOTICE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ngToast.info() 로 임시 토스트 메시지&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;설계 패턴&lt;/b&gt;&lt;br /&gt;WebSocket &amp;rarr; Angular Event Bus &amp;rarr; 각 서브 컴포넌트(노트북&amp;middot;에디터&amp;middot;인터프리터 패널 등)가 수신&lt;br /&gt;따라서 UI 레벨 컴포넌트와 통신 계층(WebSocket)이 느슨하게 결합.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결 오류 및 종료 처리&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;websocketCalls.ws.onError(() =&amp;gt; {
  $rootScope.$broadcast('setConnectedStatus', false);
});
websocketCalls.ws.onClose(() =&amp;gt; {
  clearInterval(pingIntervalId); // heartbeat 중단
  $rootScope.$broadcast('setConnectedStatus', false);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;에러&amp;middot;Close 모두 UI에 &amp;ldquo;Disconnected&amp;rdquo; 상태를 표시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정상 종료(코드 1000) 여부 판단 X &amp;rarr; 개선 여지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;퍼사드 객체 websocketCalls 공개 API&amp;nbsp;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;sendNewEvent(data)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인증 + msgId 붙여 전송&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;isConnected()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;readyState === 1 (OPEN) 검사&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ws&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;raw $websocket 인스턴스 (필드)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;테스트 관점&lt;/b&gt;&lt;br /&gt;팩토리를 mocking 해 통합 테스트 때 실 WebSocket 대신 가짜 객체 주입할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;1. 거대한 if-else 문&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;switch 또는 const handlers = { NOTE:fn, ... } 맵 구조로 교체 시 가독성/성능&amp;uarr;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 이벤트 네이밍 상수화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;op 문자열 하드코딩 대신 ENUM 객체.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 섀도잉 버그&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;const uniqueClientId = ... &amp;rarr; const uniqueIdFromMsg = ... 로 수정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 자동 reconnect 로직 세분화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;지수백오프(Exponential Back-off)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;네트워크 offline 감지(window.navigator.onLine)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. 메모리 누수 방지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AngularJS destroy 시 $scope.$on('$destroy', ...) 내부에서 WebSocket 정리 추천.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6. 타입 명세 (JSDoc + TypeScript 도입) 로 DTO 구조 명확화.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Zeppelin 아키텍처 내 위치&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────┐          REST + WebSocket      ┌────────────┐
│  Angular UI  │  &amp;harr;   (Authentication ticket)  │ Zeppelin Server │
└──────────────┘                                └────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;노트 편집기, 작업 진행률, 실시간 로그 스트림 등 상태 변화가 빈번한 영역을 WebSocket 으로 push.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;websocketEvents 팩토리는 싱글 엔드포인트를 캡슐화해 전 애플리케이션이 공유.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;websocketEvents는 실시간 협업&amp;middot;실행 상태 UI의 핵심 관문으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;세션 인증 메타데이터 주입&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메시지-ID 기반 요청&amp;ndash;응답 매칭&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Heartbeat 유지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Angular Event Bus 브로드캐스트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;를 담당함.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1108</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-zeppelinzeppelin-websrccomponentswebsocketwebsocket-eventfactoryjs-%EB%B6%84%EC%84%9D#entry1108comment</comments>
      <pubDate>Fri, 20 Jun 2025 22:48:41 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] .github/actions/tune-runner-vm/action.yml 분석</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-githubactionstune-runner-vmactionyml-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;.github/actions/tune-runner-vm/action.yml&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GitHub Actions &amp;ldquo;Composite Action&amp;rdquo; 내에서 리눅스 러너(특히 GitHub-hosted Ubuntu 러너)를 부팅한 즉시 / 초기에 실행되는 셸 스크립트를 정의함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;핵심 목적은 러너 자신의 호스트네임 &amp;rarr; IP 역방향 조회가 제대로 해결되지 않아 발생할 수 있는 DNS 지연을 없애는 것임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Composite Action 컨텍스트&lt;/h2&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;name: Tune Runner VM performance
description: tunes the GitHub Runner VM operation system
runs:
  using: composite
  steps:
    - run: |
        ...
      shell: bash&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;runs.using: composite&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여러 개의 셸 명령, 액션을 한 번에 재사용하도록 묶은 &amp;ldquo;재사용형 액션&amp;rdquo;임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 Composite Action을 리포지토리의 .github/actions/&amp;lt;action-dir&amp;gt; 아래 두고, 워크플로우에서 uses: ./.github/actions/&amp;lt;action-dir&amp;gt; 형태로 호출하면 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bash 스니펫 작동 원리&lt;/h2&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;if [[ &quot;$OSTYPE&quot; == &quot;linux-gnu&quot;* ]]; then
  echo -e &quot;$(ip addr show eth0 | grep &quot;inet\b&quot; | awk '{print $2}' | cut -d/ -f1)\t$(hostname -f) $(hostname -s)&quot; \
  | sudo tee -a /etc/hosts
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;if [[ &quot;$OSTYPE&quot; == &quot;linux-gnu&quot;* ]]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;리눅스에서만 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;macOS&amp;middot;Windows 러너는 건너뜀&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ip addr show eth0&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;네트워크 인터페이스 eth0 정보 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GitHub Ubuntu runner는 기본 NIC가 eth0&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;grep &quot;inet\b&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;IPv4 주소 라인만 필터&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;inet6(IPv6) 제외&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;awk '{print $2}'&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;lt;IP/CIDR&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;필드 추출&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예: 192.168.0.10/20&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;cut -d/ -f1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CIDR 접미어 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;순수 IP(192.168.0.10)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;hostname -f&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;FQDN(fully-qualified domain name)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예: ip-192-168-0-10.ec2.internal&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;hostname -s&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;짧은 호스트네임&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예: ip-192-168-0-10&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;echo&amp;nbsp;-e&amp;nbsp;&quot;&amp;lt;IP&amp;gt;\t&amp;lt;FQDN&amp;gt;&amp;nbsp;&amp;lt;SHORT&amp;gt;&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;탭으로 구분한 한 줄 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;/etc/hosts 규격 맞춤&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;sudo tee -a /etc/hosts&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;root 권한으로 파일 덧붙임&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;런타임에 /etc/hosts 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;결과 줄 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;192.168.0.10    ip-192-168-0-10.ec2.internal ip-192-168-0-10
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;역방향 DNS 지연 제거&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일부 CLI 도구(예: apt, git, Java 애플리케이션)는 내부적으로 getaddrinfo() &amp;rarr; reverse lookup을 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;러너의 기본 /etc/hosts에는 짧은 호스트네임만 있거나 FQDN이 빠져 있어, 외부 DNS로 질의가 전파 &amp;rarr; 수 ms ~ 수 초 지연이 생길 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;로그, 빌드 툴 호환성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Jenkins/Gradle/Maven처럼 hostname -f를 기대하는 툴이 간헐적으로 에러나 경고를 출력하는 문제를 예방함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Ephemeral VM&lt;span&gt;&amp;nbsp;&lt;/span&gt;특성상 안전&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GitHub-hosted 러너는 작업 종료 후 삭제되므로 /etc/hosts 영구 변경에 대한 리스크가 없음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;인터페이스 이름&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;최신 Ubuntu에서 eth0 대신 ens5, ens160 등 예외가 있을 수 있으므로, `ip -o -4 addr show up primary&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;중복 엔트리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;워크플로가 같은 러너에서 여러 번 실행될 경우 중복 라인 생성 가능. `grep -q &quot;$(hostname -f)&quot; /etc/hosts&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;권한 상승&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;sudo 사용은 composite action 내부이므로 비교적 안전하지만, self-hosted 러너라면 조직 정책에 맞춰 NOPASSWD 설정 확인 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 예시 (범용성 및 멱등성 확보)&lt;/h2&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;if [[ &quot;$OSTYPE&quot; == &quot;linux-gnu&quot;* ]]; then
  ip=&quot;$(hostname -I | awk '{print $1}')&quot;           # 첫 번째 활성 IPv4
  fqdn=&quot;$(hostname -f)&quot;
  short=&quot;$(hostname -s)&quot;
  line=&quot;$ip\t$fqdn $short&quot;

  if ! grep -qE &quot;^\s*$ip\s+$fqdn\b&quot; /etc/hosts; then
    echo -e &quot;$line&quot; | sudo tee -a /etc/hosts
  fi
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NIC 이름을 하드코딩하지 않고 hostname -I 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이미 동일 엔트리가 있으면 추가하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;이 Composite Action 단계는 리눅스 GitHub 러너의 DNS 자체 해결 속도를 개선하기 위해,&lt;br /&gt;/etc/hosts 에 FQDN shortname 라인을 동적으로 추가함.&lt;br /&gt;DNS 타임아웃으로 인한 불필요한 빌드 지연이나 네트워크 콜 레이턴시를 없애 &amp;ldquo;러너 성능&amp;rdquo;을 체감적으로 향상시키는 미세 조정(tuning) 테크닉임.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1107</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-githubactionstune-runner-vmactionyml-%EB%B6%84%EC%84%9D#entry1107comment</comments>
      <pubDate>Sat, 14 Jun 2025 20:28:59 +0900</pubDate>
    </item>
    <item>
      <title>[Computer] CondaError: Run 'conda init' before 'conda activate'</title>
      <link>https://pinggoopark.tistory.com/entry/Computer-CondaError-Run-conda-init-before-conda-activate</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;base 활성화시 에러&lt;/p&gt;
&lt;pre id=&quot;code_1749862522248&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;conda activate base
# CondaError: Run 'conda init' before 'conda activate'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해결하는 명령어 입력&lt;/p&gt;
&lt;pre id=&quot;code_1749862668398&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;conda init zsh
# no change     /opt/homebrew/Caskroom/miniconda/base/condabin/conda
# no change     /opt/homebrew/Caskroom/miniconda/base/bin/conda
# no change     /opt/homebrew/Caskroom/miniconda/base/bin/conda-env
# no change     /opt/homebrew/Caskroom/miniconda/base/bin/activate
# no change     /opt/homebrew/Caskroom/miniconda/base/bin/deactivate
# no change     /opt/homebrew/Caskroom/miniconda/base/etc/profile.d/conda.sh
# no change     /opt/homebrew/Caskroom/miniconda/base/etc/fish/conf.d/conda.fish
# no change     /opt/homebrew/Caskroom/miniconda/base/shell/condabin/Conda.psm1
# no change     /opt/homebrew/Caskroom/miniconda/base/shell/condabin/conda-hook.ps1
# no change     /opt/homebrew/Caskroom/miniconda/base/lib/python3.13/site-packages/xontrib/conda.xsh
# no change     /opt/homebrew/Caskroom/miniconda/base/etc/profile.d/conda.csh
# no change     /Users/pgt0409/.zshrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해결하는 명령어 입력&lt;/p&gt;
&lt;pre id=&quot;code_1749862755403&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(base) ➜  ~ conda activate base
(base) ➜  ~&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Operating System/Computer</category>
      <category>computer</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1106</guid>
      <comments>https://pinggoopark.tistory.com/entry/Computer-CondaError-Run-conda-init-before-conda-activate#entry1106comment</comments>
      <pubDate>Sat, 14 Jun 2025 09:59:20 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] Github Action에서 Chrome/ChromeDriver 버전 고정</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-Github-Action%EC%97%90%EC%84%9C-ChromeChromeDriver-%EB%B2%84%EC%A0%84-%EA%B3%A0%EC%A0%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;버전 고정이 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CI 환경(특히 ubuntu-latest 러너)은 크롬 계열 브라우저와 드라이버를 자동 업데이트함.&lt;br /&gt;Selenium, Playwright, Puppeteer 테스트가 다음과 같은 증상을 보인다면 의심해 봐야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 테스트가 갑자기 Unknown error: cannot find Chrome binary 로 실패&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;러너의 크롬은 업데이트됐지만 드라이버는 구버전&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. session not created: This version of ChromeDriver only supports Chrome X&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;드라이버는 최신이지만 크롬이 구버전&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 헤드리스 모드에서 렌더링 차이 및 스크린샷 불일치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메이저 버전 간 렌더링 엔진 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 신뢰도를 높이려면 테스트 대상과 자동화 드라이버를 같은 메이저 버전으로 &amp;ldquo;묶어&amp;rdquo; 두는 것이 최선임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워크플로우&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#   핵심 단계만 발췌
- name: Print Chrome and ChromeDriver version
  run: |
    google-chrome --version || chromium-browser --version
    chromedriver --version

- name: Downgrade to Chrome &amp;amp; ChromeDriver 125
  run: |
    # ① 기존 바이너리 정리
    sudo apt-get remove -y google-chrome-stable || true
    sudo rm -f /usr/bin/google-chrome /usr/bin/chromedriver

    # ② Chrome 125 설치
    wget https://.../chrome-linux64.zip
    unzip chrome-linux64.zip
    sudo mv chrome-linux64 /opt/chrome125
    sudo ln -sf /opt/chrome125/chrome /usr/bin/google-chrome

    # ③ ChromeDriver 125 설치
    wget https://.../chromedriver-linux64.zip
    unzip chromedriver-linux64.zip
    sudo mv chromedriver-linux64/chromedriver /usr/bin/chromedriver
    sudo chmod +x /usr/bin/chromedriver
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동작 순서는 다음과 같음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 현재 버전 출력&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;디버깅 편의성을 위해 기존 브라우저&amp;middot;드라이버 버전을 기록함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 기존 패키지 제거&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;apt-get remove(패키지) + rm -f(심볼릭 링크)로 잔재를 정리함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 크롬 125 수동 설치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;공식 &amp;ldquo;Chrome for Testing&amp;rdquo; 아티팩트를 사용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;/opt/chrome125에 압축 해제 후 /usr/bin/google-chrome 심볼릭 링크를 새로 만듬.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. ChromeDriver 125 수동 설치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동일 버전 드라이버를 다운로드&amp;middot;배치하여 바이너리 호환을 보장함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. 버전 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설치가 정상인지 두 바이너리의 버전을 다시 출력함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;1. Chrome for Testing 패키지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2023년 이후, 구글은 CI용 무서명 아카이브를 &lt;a href=&quot;https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/&quot;&gt;https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/&lt;/a&gt; 하위에 제공.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메이저, 마이너, 패치까지 모두 고정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ZIP 내 구조가 chrome-linux64/ 아래로 통일.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 심볼릭 링크 재작성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우분투 러너는 기본적으로 /usr/bin/google-chrome를 통해 브라우저를 찾음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ln -sf로 덮어쓰면 PATH를 변경할 필요 없음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. apt-get remove 를 쓰는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;구글 레포를 통해 자동 설치된 패키지 버전이 선행된다면, 수동 설치한 바이너리가 실행되지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;--purge 옵션을 추가해 설정파일까지 지울 수도 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 드라이버 실행 권한&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다운받은 chromedriver에는 실행 비트가 빠질 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;chmod +x를 반드시 수행.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. Headless 테스트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Chrome 115부터는 --headless=new 플래그가 기본값.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Selenium 4.21+부터 자동 적용되므로 버전을 맞춘 뒤 추가 설정 불필요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버전 업, 다운그레이드 자동화 전략&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 매트릭스 전략여러 버전을 병렬로 돌려 브레이킹 체인지 여부를 즉시 파악함.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. strategy&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;matrix: chrome-version: [125.0.6422.60, 124.0.6367.91]&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 캐싱으로 속도 개선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;actions/cache를 이용해 압축 파일을 저장 &amp;rarr; wget 생략.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;키를 chrome-${{ matrix.chrome-version }} 식으로 설정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Periodic &amp;ldquo;canary&amp;rdquo; 워크플로&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;매주 최신 버전을 받아 테스트 &amp;rarr; 실패 시 슬랙 알림.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안정 브랜치에서는 여전히 고정 버전 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;1. chromedriver: command not found&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;경로가 /usr/bin에 없거나 chmod +x 누락.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;경로 확인, 권한 부여.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. chmod: Operation not permitted&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;워크플로에서 sudo 없이 /usr/bin 수정 시도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;sudo 사용 또는 $HOME/bin에 설치 후 PATH 갱신.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Chrome 126 릴리스로 테스트 전량 실패&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자동 업데이트를 허용한 셀프호스티드 러너.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;버전 핀(pin) 전략 도입, 자동 업데이트 비활성화.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;브라우저&amp;ndash;드라이버 버전 불일치는 E2E 테스트의 대표적 &amp;lsquo;플레이크&amp;rsquo; 원인임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GitHub Actions에서 수동 아티팩트 설치 + 심볼릭 링크 방식으로 완전 고정하면 재현 가능한 빌드 환경을 확보할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;배포 프레임워크에 맞춰 매트릭스&amp;middot;캐싱&amp;middot;주기적 최신 버전 탐색을 조합하면 안정성과 미래 대비를 모두 지킬 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CI 파이프라인에서 테스트 신뢰도를 지키고 싶다면, &amp;ldquo;브라우저&amp;middot;드라이버 버전 관리&amp;rdquo;를 빌드 스크립트의 첫 줄에 포함시켜야함.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1105</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-Github-Action%EC%97%90%EC%84%9C-ChromeChromeDriver-%EB%B2%84%EC%A0%84-%EA%B3%A0%EC%A0%95#entry1105comment</comments>
      <pubDate>Thu, 12 Jun 2025 21:21:53 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] SparkUtils.java 분석</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-SparkUtilsjava-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스의 역할과 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SparkUtils 는 Zeppelin &amp;harr; Spark 통합 계층에서 Spark 1.x &amp;harr; Spark 2.x/3.x API 차이를 흡수하기 위해 설계된 추상(base) 유틸리티 클래스임. 실제 런타임에서는 리플렉션으로 Spark 버전에 맞는 구체 서브클래스(Spark1Utils, Spark2Utils 등)를 로드하지만, 공통으로 재사용할 수 있는 기능들은 이 베이스 클래스에 구현돼 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주요 책임은 세 가지임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Job Tracking&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SparkListener를 등록해 JobStart 이벤트가 발생할 때마다 Spark Web UI (URL) 정보를 노트/문단 단위로 Zeppelin 프론트엔드에 push.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;DataFrame 렌더링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Dataset&amp;lt;Row&amp;gt;를 Zeppelin 프론트엔드가 인식하는 %table 형식 문자열로 직렬화 (showDataFrame).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;YARN 호환성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Hadoop(YARN) 버전에 따라 Spark Web UI 경로가 달라지는 문제(YARN-6615) 보정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필드 및 상수&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;protected SparkSession sparkSession;
protected Properties  properties;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;sparkSession&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재 Interpreter 인스턴스가 공유하는 SparkSession.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;properties&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인터프리터 설정(예: zeppelin.spark.ui.hidden) 전달용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// YARN-6615 이전/이후 버전 비교용 마일스톤
private static final String HADOOP_VERSION_2_6_6 = &quot;2.6.6&quot;;
// &amp;hellip;
private static final String HADOOP_VERSION_3_0_0 = &quot;3.0.0&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;VersionUtil.compareVersions()로 YARN 릴리스 간 범위를 계산할 때 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성자&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public SparkUtils(Properties properties, SparkSession sparkSession)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin interpreter 가 초기화될 때 전달되는 설정(Properties)과 SparkSession을 저장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;setupSparkListener(...)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public void setupSparkListener(String master,
                               String sparkWebUrl,
                               InterpreterContext ctx)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;흐름&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. SparkContext.getOrCreate()로 현재 컨텍스트 획득.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 익명 SparkListener 를 등록:&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2-1. 이벤트: onJobStart(SparkListenerJobStart jobStart)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2-2. 조건:&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;spark.ui.enabled == true&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;zeppelin.spark.ui.hidden != true&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 조건을 만족하면 buildSparkJobUrl(...) 호출 &amp;rarr; Zeppelin 프론트엔드로 Job 하이퍼링크 전송.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 함으로써 문단 실행 중 생성되는 각 Spark Job이 자기 결과 셀 옆에 &amp;ldquo;SPARK JOB&amp;rdquo; 버튼으로 노출됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;showDataFrame(...)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public String showDataFrame(Object obj, int maxResult, InterpreterContext ctx)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Dataset / DataFrame &amp;rarr; Zeppelin 출력으로 직렬화하는 핵심 로직.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주요 단계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 타입 판별&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;obj가 Dataset(또는 DataFrame)인지 체크.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. Template 처리 (template local-prop):&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 %template myTemplate 형태로 지정한 경우,&lt;br /&gt;첫 Row만 추출하여 SingleRowInterpreterResult로 HTML 렌더링 후 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 테이블 직렬화:&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(maxResult + 1) 개 Row fetch &amp;rarr; 결과가 limit 초과인지 판단.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1번째 Row를 헤더로 정규화(TableDataUtils.normalizeColumns).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 셀 값을 TableDataUtils.normalizeColumn으로 문자열화.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;초과 시 %text 영역에 ResultMessages.getExceedsLimitRowsMessage(...) 추가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. Non-DataFrame fallback&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비지원 타입이면 toString() 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;팁&lt;/b&gt;&lt;br /&gt;여기서 %table &amp;rArr; Zeppelin Markdown 문법.&lt;br /&gt;프론트엔드는 이 마커를 읽어 DataGrid 컴포넌트로 변환함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getAsDataFrame(String value)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Zeppelin UI가 %table 결과를 역직렬화 해 다시 Spark DataFrame으로 쓰고 싶을 때 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. [헤더 1줄] + [데이터 N줄] 탭-구분 문자열을 파싱해&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2-1. StructType schema 동적 생성(모두 StringType)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2-2. GenericRow 배열로 변환 후 sparkSession.createDataFrame(...).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spark Job URL 빌드 -buildSparkJobUrl(...)&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 기본 규칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자 설정 sparkWebUrl에 {jobId} 토큰이 있으면 치환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;없으면 &amp;hellip;/jobs/job?id=&amp;lt;jobId&amp;gt; 패턴 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. YARN-6615 미패치 클러스터 보호&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;YARN 2.7.0 이전 등 일부 버전은 /jobs/job?id=&amp;hellip; URL 지원 안 함 &amp;rarr; /jobs 루트로 폴백.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로직은 supportYarn6615(String version) 헬퍼에서 판별.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. Zeppelin 프론트엔드 통신&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;jobGroupId(Spark conf spark.jobGroup.id) &amp;rarr; NoteID | ParagraphID 추출.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OnParaInfosReceived gRPC 이벤트 발행 &amp;rarr; React UI가 Job 링크 표시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getNoteId, getParagraphId&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. jobGroupId 포맷:&lt;br /&gt;replName | sessionId | noteId | paragraphId&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 파싱 후 Note/Paragraph 스코프 자바 식별자 반환.&lt;br /&gt;&amp;ndash; 잘못된 형식이면 RuntimeException.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;supportYarn6615(...)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;YARN-6615&lt;/b&gt;&lt;br /&gt;&amp;ldquo;Add per-job page in the Web UI that uses job ID instead of submission ID&amp;rdquo;&lt;br /&gt;(Hadoop 2.7.0 이후 정식 반영, 일부 백포트 포함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메서드는 &lt;b&gt;&amp;ldquo;현재 실행 중인 Hadoop 버전이 해당 패치 포함 여부&amp;rdquo;&lt;/b&gt;를 계산.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;VersionUtil.compareVersions(a, b) (Hadoop 유틸)로 범위 비교.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조건이 참이면 &amp;ldquo;패치 적용됨&amp;rdquo; &amp;rarr; 직렬 /jobs/job?id=&amp;lt;jobId&amp;gt; 경로 사용 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;Thread Safety&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;setupSparkListener가 한 번 이상 호출돼도 SparkContext가 싱글턴이라 listener 중복 가능성 있음. 인터프리터 생성 로직에서 idempotent 보장 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Row Limit 정책&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;spark.sql.debug.maxToStringFields 기준 변환 실패 가능성 &amp;rarr; 큰 구조체/배열 컬럼 렌더링 시 주의&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Binary Type 지원 부족&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;%table serialization이 문자열 전제. BinaryType, Array[Byte] 컬럼이 있으면 Base64 인코딩을 사용자 쪽에서 따로 처리해야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Schema 추론 단순화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;getAsDataFrame은 모든 컬럼을 StringType으로 고정 &amp;ndash; 타입 손실을 감수한 디자인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SparkUtils는 Zeppelin Spark Interpreter의 다음 두 축을 연결하는 접착제임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 실행 계층&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spark Job lifecycle hook (SparkListener)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 표시 계층&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin 노트북 UI 테이블 및 버튼 렌더링&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 통해 사용자는 별다른 설정 없이도 스칼라&amp;middot;파이썬&amp;middot;SQL 문단 결과를 자동으로 테이블로 확인하고, 각 Job을 즉시 Spark UI에서 추적할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 클러스터 환경이 오래된 Hadoop 배포판이라도 YARN-6615 패치 여부를 동적으로 탐지해 잘못된 링크 노출을 방지함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;확장 방법은 아래와 같음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. Spark 3.x 신규 기능(Adaptive Query Execution Metrics 등)을 UI에 노출하려면 listener 콜백 확장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. %table 외 다양한 데이터 포맷(CSV, Parquet 샘플 등)을 렌더링하려면 showDataFrame 오버라이드.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1104</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-SparkUtilsjava-%EB%B6%84%EC%84%9D#entry1104comment</comments>
      <pubDate>Sat, 7 Jun 2025 19:23:02 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] SparkSubmitInterpreter.java 분석</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-SparkSubmitInterpreterjava-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spark Submit Interpreter가 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SparkSubmitInterpreter는 Apache Zeppelin의 Interpreter Framework 안에서 %spark-submit 매직 명령을 제공하는 클래스임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 Zeppelin 노트북 셀(Paragraph)에서 Spark CLI( spark-submit )를 그대로 호출할 수 있게 해 줌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin &amp;harr; Shell &amp;harr; Spark 흐름을 중개하며, Spark UI URL, YARN Application ID 등 실시간 메타데이터를 추출, 전송해 Zeppelin 프런트엔드에 작업 상태를 시각화함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spark 클러스터 유형(YARN, Stand-alone, Local 등)에 상관없이 동작하도록 설계돼 있지만, YARN 모드일 때는 별도로 잡 종료( yarn application -kill )까지 지원함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. extends ShellInterpreter&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존 ShellInterpreter(bash &amp;middot; sh 등 쉘 명령 실행기)를 상속해, &amp;ldquo;shell + spark-submit&amp;rdquo; 패턴을 재활용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. sparkHome&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SPARK_HOME 설정을 캐시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Spark 설치 루트의 bin/spark-submit 검증에 사용됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. yarnAppIdMap: ConcurrentMap&amp;lt;String,String&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Paragraph ID &amp;rarr; YARN App ID 매핑.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Thread-safe (ConcurrentHashMap) 라서 동시 실행에도 안전함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. LOGGER&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SLF4J 로깅.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;INFO 수준으로 Spark 명령, DEBUG 수준으로 로그 파싱 내용을 남김.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생성자&lt;/h2&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;public SparkSubmitInterpreter(Properties property) {
  super(property);
  setProperty(&quot;shell.command.timeout.millisecs&quot;, String.valueOf(Integer.MAX_VALUE));
  this.sparkHome = properties.getProperty(&quot;SPARK_HOME&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ShellInterpreter 초기화 후, 무한대 (Integer.MAX_VALUE) 타임아웃을 설정해 장시간 Spark 잡도 끊기지 않게 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SPARK_HOME을 읽어 두고, 초기 로깅으로 실 배포 구성 오류를 조기 감지함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;internal Interpreter 동작 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 빈 입력 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SUCCESS 즉시 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 전제 조건 검사&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SPARK_HOME 미설정 &amp;rarr; ERROR.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;bin/spark-submit 실행 파일 존재 여부 확인 &amp;rarr; 없으면 ERROR.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 명령 빌드&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;lt;SPARK_HOME&amp;gt;/bin/spark-submit + &amp;lt;사용자 인자(cmd.trim())&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. 출력 리스너 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;context.out.addInterpreterOutListener(new SparkSubmitOutputListener(context))&lt;br /&gt;&amp;rarr; Spark 로그를 실시간 가로채 Spark UI URL&amp;middot;YARN ID를 파싱함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. ShellInterpreter 실행 &amp;rarr; 부모 클래스에 최종 CLI 전달.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;6. 정리 &amp;rarr; 실행 완료 후 yarnAppIdMap에서 Paragraph ID 키 제거(메모리 누수 방지).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;취소 로직 cancel&lt;/h2&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;public void cancel(InterpreterContext context)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ShellInterpreter 표준 취소를 먼저 호출해 Spark 프로세스 자체를 끊음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추가로 YARN App ID가 매핑돼 있으면명령을 Runtime.exec으로 실행해 클러스터 자원을 확실히 반환함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;yarn application -kill &amp;lt;appId&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;YARN CLI가 $PATH에 없거나 권한이 부족하면 WARN 로그만 남기고 실패를 무시함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내부 클래스로 구현된 SparkSubmitOutputListener&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;spark-submit STDOUT/STDERR 스트림을 한 줄씩 검사해 메타데이터를 추출하고 Zeppelin 프런트엔드로 이벤트 전파.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &quot;tracking URL:&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;YARN 모드에서 프록시 URL 검출&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;tracking URL: http://&amp;hellip;/proxy/application_12345/&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. &quot;Bound SparkUI to&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Standalone/Local 모드 UI URL 검출&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Bound SparkUI to 0.0.0.0, and started at http://host:4040&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. 조건 만족 시 buildSparkUIInfo(...) 호출.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 공백 기준 최우측 토큰을 잘라 URL 추정(마지막 문자 &quot;)&quot; 같은 구두점 제거).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. InterpreterContext.getIntpEventClient().onParaInfosReceived(infos)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3-1. infos Map 키&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3-2. jobUrl : http://... label : Spark UI tooltip: View in Spark web UI noteId, paraId&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3-3. Zeppelin 프런트엔드가 Paragraph 상단에 Spark UI 버튼을 렌더링합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. URL 끝 슬래시 뒤 토큰이 application_&amp;lt;id&amp;gt; 형태면 YARN App ID 추출 &amp;rarr; yarnAppIdMap 저장.&lt;br /&gt;&amp;rarr; 나중에 cancel 시 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;한계&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;순수 문자열 포함 여부만 검사 &amp;rarr; 로그 포맷이 변경되면 실패 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;K8s 모드(&amp;ldquo;driver pod &amp;rdquo;) 등은 아직 미지원.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. 실행 병렬성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 Paragraph마다 독립 InterpreterContext + OutputListener로 격리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. yarnAppIdMap&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ConcurrentHashMap 덕분에 다중 Paragraph가 동시에 쓰더라도 경합 없이 안전.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 완료/취소 후 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Map 항목 및 Listener 플래그(isSparkUrlSent)를 일시적으로만 유지, 메모리 낭비 최소화.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. 필수 환경 오류 (SPARK_HOME 누락&amp;middot;spark-submit 불존재)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;InterpreterResult.Code.ERROR로 사용자가 즉시 인지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 실행 중 예외&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;부모 ShellInterpreter에게 위임하므로, 표준 Zeppelin Shell 오류 화면이 그대로 노출.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. YARN Kill 실패&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자에게 직접 오류를 터뜨리지 않고 WARN 로그만 기록해 UX 저하를 방지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. Zeppelin Interpreter 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SPARK_HOME : 클러스터 노드 전체에 동일하게 배포돼 있어야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;HADOOP_CONF_DIR : YARN 클러스터 접속이 필요하면 포함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. Zeppelin 사용자 계정 권한&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;spark-submit, yarn 명령 모두 접근 가능해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 로그 보존&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OutputListener가 파싱 실패할 경우를 대비해 paragraph raw log를 수집하면 디버깅이 용이함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;1. 파싱 로직&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정규식&amp;middot;상태 머신으로 개선해 로그 포맷 변화에 강건하게.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. K8s 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Submitted Spark application&quot;&amp;middot;driver pod name is 등 K8s 전용 로그 패턴 추가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 보안&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;문자열 파이프라인에 쉘 인젝션 위험 점검(사용자 cmd sanitize).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Listener 클래스를 별도 파일/패키지로 분리해 테스트 가능성 향상.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5. Timeout&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자 지정 타임아웃 옵션 노출(무한대 고정은 운영 정책에 따라 과도할 수 있음).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;SparkSubmitInterpreter는 Zeppelin 노트북에서 &amp;ldquo;CLI 수준의 유연성 + Zeppelin UI 경험&amp;rdquo; 두 마리 토끼를 잡기 위해 설계된 가교 컴포넌트임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ShellInterpreter 상속으로 복잡한 프로세스 관리 로직을 재사용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OutputListener로 Spark 실행 메타데이터를 실시간 추출&amp;nbsp;및 시각화하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;YARN App ID 추적/취소 기능까지 포함해 운영 편의성을 높임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 구조를 정확히 이해하면, Zeppelin &amp;harr; Spark 통합을 커스텀하거나 신규 클러스터 모드(K8s, Mesos)로 확장할 때 안정적으로 변형 및 기여할 수 있음.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1103</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-SparkSubmitInterpreterjava-%EB%B6%84%EC%84%9D#entry1103comment</comments>
      <pubDate>Tue, 3 Jun 2025 12:00:37 +0900</pubDate>
    </item>
    <item>
      <title>[Zeppelin] ElasticsearchClient.java 분석</title>
      <link>https://pinggoopark.tistory.com/entry/Zeppelin-ElasticsearchClientjava-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;원본 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;zeppelin/elasticsearch/src/main/java/org/apache/zeppelin/elasticsearch/client/ElasticsearchClient.java&lt;/p&gt;
&lt;pre id=&quot;code_1748087654951&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the &quot;License&quot;); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.zeppelin.elasticsearch.client;

import org.apache.zeppelin.elasticsearch.action.ActionResponse;

/**
 * Interface that must be implemented by any kind of Elasticsearch client (transport, ...).
 */
public interface ElasticsearchClient {

  ActionResponse get(String index, String type, String id);

  ActionResponse index(String index, String type, String id, String data);

  ActionResponse delete(String index, String type, String id);

  ActionResponse search(String[] indices, String[] types, String query, int size);

  void close();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;이 인터페이스는 Apache Zeppelin의 Elasticsearch Interpreter 모듈에 포함됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;노트북에서 %elasticsearch 구문을 실행할 때 실제 클러스터와 통신하는 클라이언트 계층의 추상화를 담당함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin-내에서는 REST High Level Client, Transport Client, 신규 Java API Client 같은 여러 구현체를 이 인터페이스 뒤에 숨겨 두어, 상위 계층(파서&amp;middot;렌더러)이 Elasticsearch 버전이나 접속 방식에 종속되지 않도록 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;get&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 핵심 파라미터: index, type, id&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 반환형: ActionResponse&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 의미: 단건 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;index&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 핵심 파라미터: index, type, id, data&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 반환형: ActionResponse&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 의미: 생성, 업서트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;delete&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 핵심 파라미터: index, type, id&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 반환형: ActionResponse&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 의미: 단건 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;search&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 핵심 파라미터: indices[], types[], query, size&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 반환형: ActionResponse&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 의미: 다건 검색&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;close&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 핵심 파라미터: -&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 반환형: void&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 의미: 자원 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;ActionResponse 추상화&lt;/b&gt;&lt;br /&gt;Zeppelin 내부 포맷으로 응답을 래핑합니다. 구현체가 GetResponse, IndexResponse 등 Elasticsearch 고유 객체를 직접 노출하지 않아도 되므로, 버전 업그레이드 시 컴파일 의존성을 줄임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;type 파라미터&lt;/b&gt;&lt;br /&gt;ES 6.x부터 단일 타입, ES 8.x에선 완전 제거되었기 때문에 현대 버전과 호환하려면 &quot;type&quot; 자리는 _doc 고정값 혹은 메서드 제거가 필요함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;동기식 API&lt;/b&gt;&lt;br /&gt;모든 메서드가 즉시 ActionResponse 를 반환하므로 호출 스레드가 블로킹됨. 대량 병렬 처리나 GUI-backed 사용자 입력이 잦은 Zeppelin 환경에선 비동기, Reactive 변형(CompletableFuture/Project Reactor 등)도 고려할 만함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;스트림, 배치 API 부재&lt;/b&gt;&lt;br /&gt;Scroll, Search After, Bulk API, Mget/Mdelete 등 ES가 제공하는 핵심 기능이 노출되지 않아, 대용량 데이터 로딩 시 추가 래퍼가 필요함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전형적인 구현체 예시&lt;/h2&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;public class RestElasticsearchClient implements ElasticsearchClient, AutoCloseable {
  private final RestHighLevelClient client;

  public ActionResponse index(String index, String type, String id, String data) {
      IndexRequest req = new IndexRequest(index).id(id)
          .source(data, XContentType.JSON);
      IndexResponse rsp = client.index(req, RequestOptions.DEFAULT);
      return new ActionResponse(rsp); // Zeppelin 래퍼
  }
  /* get, delete, search 동일 패턴 */
  public void close() { client.close(); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;TransportElasticsearchClient (ES &amp;lt; 7.0)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내부적으로 org.elasticsearch.client.transport.TransportClient&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;JavaApiElasticsearchClient (ES 8.x)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;org.elasticsearch.client.elasticsearch-java 기반&lt;br /&gt;REST High Level Client가 7.15부터 deprecated 되었으므로 장기적으로는 이 구현만 유지하는 것이 안전함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Thread-safety와 자원관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Implementer는 connection pool(Apache HttpAsyncClient 등)과 I/O 스레드를 재사용해야 하며, 노트북 세션별 클라이언트 다중 생성 시 리소스 누수를 방지하기 위해 Interpreter.close() 단계에서 client.close() 호출을 전파해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zeppelin은 여러 사용자가 동시에 같은 Interpreter 인스턴스를 공용할 수 있으므로, 구현체는 stateless 하거나 자체 동기화(예: synchronized, ReentrantLock)를 적용해야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현대 Elasticsearch 8.x 이후와의 호환성 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Mapping Type&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 기존 코드: &quot;type&quot; 인자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. ES 7-&amp;gt;8: 완전 제거, _doc 고정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 개선 방안: 인터페이스에서 type 파라미터를 없애거나, default 값 _doc 하드코딩&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Java Client&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 기존 코드: Transport / HLRC&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. ES 7-&amp;gt;8: Transport Client 삭제, HLRC deprecated&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 개선 방안: Elasticsearch Java API Client 로 교체&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;검색 DSL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 기존 코드: String query&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. ES 7-&amp;gt;8: 멀티 필드 쿼리, Aggregation, Sort 등 미지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 개선 방안: SearchRequest.Builder 를 받는 오버로드 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Async/Bulk&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기존 코드: 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. ES 7-&amp;gt;8: 고속 처리 요구 증가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 개선 방안: 비동기 및 Bulk 메서드 추가, 반환값을 CompletableFuture&amp;lt;ActionResponse&amp;gt; 로 확장&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;인터페이스 분할&lt;/b&gt;&lt;br /&gt;CRUD + 검색을 하나로 묶으면 인터페이스 응집도가 낮아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DocumentOperations (get/index/delete)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SearchOperations (search/scroll/msearch)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;LifecycleOperations (close, ping, info)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;제네릭 응답&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748088372113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;T extends ActionResponse&amp;gt; T index(...);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;으로 캐스팅 비용 없이 구체 응답 타입 접근 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;AutoCloseable 상속&lt;/b&gt;&lt;br /&gt;extends AutoCloseable 로 선언하면 try-with-resources 사용성이 높아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;옵션 객체 패턴&lt;/b&gt;&lt;br /&gt;IndexOptions, SearchOptions 등 Builder 객체를 받아 메서드 매개변수 폭발을 방지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Zeppelin Interpreter 관점 사용 흐름&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;ElasticsearchClient client = ...;   // 주입
String query = &quot;{ \&quot;match_all\&quot;: {} }&quot;;
ActionResponse resp = client.search(new String[]{&quot;logs-*&quot;}, new String[]{&quot;_doc&quot;}, query, 1000);
// InterpreterResultWriter 가 resp.toJson() 을 Notebook으로 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;노트북 사용자는&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;%elasticsearch
{
  &quot;query&quot;: { &quot;match&quot;: { &quot;host&quot;: &quot;10.0.1.23&quot; } },
  &quot;size&quot;: 200
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처럼 JSON DSL을 입력하고, Interpreter가 이를 search(...) 문자열 인자로 넘기는 구조임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;ElasticsearchClient 인터페이스는 Zeppelin-내 플러그어블 클라이언트 계층의 핵심 축을 이룸.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나 타입 제거, 신규 Java API Client 도입, 비동기 요구 같은 Elasticsearch 8.x 생태계 변화를 반영하려면 다음과 같은 대대적 리팩터링이 필요함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. type 파라미터 제거 또는 _doc 고정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. ActionResponse 제네릭화 및 Builder 패턴 도입&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 비동기 + Bulk API 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. elasticsearch-java 클라이언트로 구현체를 전환&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 통해 Zeppelin 사용자는 최신 Elastic Stack에서도 무중단으로 노트북 기반 탐색&amp;middot;시각화를 계속할 수 있음.&lt;/p&gt;</description>
      <category>Data Engineering/Zeppelin</category>
      <category>Zeppelin</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1102</guid>
      <comments>https://pinggoopark.tistory.com/entry/Zeppelin-ElasticsearchClientjava-%EB%B6%84%EC%84%9D#entry1102comment</comments>
      <pubDate>Sat, 24 May 2025 21:09:44 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Iceberg, Partition Transform이 감춰진 파티션을 가능하게 하는 메커니즘</title>
      <link>https://pinggoopark.tistory.com/entry/AWS-Partition-Transform%EC%9D%B4-%EA%B0%90%EC%B6%B0%EC%A7%84-%ED%8C%8C%ED%8B%B0%EC%85%98%EC%9D%84-%EA%B0%80%EB%8A%A5%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;lsquo;감춰진 파티션( Hidden Partition )&amp;rsquo;이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Iceberg에서는 파티션 컬럼을 테이블 스키마에 노출하지 않고도 파티셔닝 효과를 얻도록 설계했음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;PARTITIONED BY (days(ts), bucket(16, id)) 처럼 Transform만 메타데이터에 기록하고, 실제 데이터 파일 경로에는 날짜&amp;middot;버킷 값이 드러나지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;덕분에 사용자는 WHERE ts BETWEEN &amp;hellip; 같은 원본 컬럼 조건만 써도 자동으로 파티션 프루닝이 이뤄짐.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메커니즘: Transform &amp;rarr; Partition Tuple &amp;rarr; Manifest&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Partition Spec&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테이블 메타데이터에 _Transform 식_과 Field-ID가 저장됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쓰기 시점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;엔진(Spark/Flink 등)이 각 레코드에 Transform을 적용해 Partition Tuple을 계산하고, 그 값을 데이터 파일의 Manifest 엔트리에 기록함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿼리 시점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;쿼리 플래너가 SQL Predicate(예: ts &amp;gt; '2025-05-17 00:00')를 같은 Transform으로 변환해 Manifest 레벨에서 파티션을 걸러낸 뒤, 남은 파일만 읽음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 과정이 전부 메타데이터 파일 조작으로 끝나므로 디렉터리 탐색이 필요 없고, 파티션 구조가 바뀌어도 과거 데이터는 그대로 유효함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hive-style 디렉터리 파티셔닝과의 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파티션 정보 저장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Hive-style: HMS에 row_per_partition + 경로 문자열&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Iceberg Hidden: 단일 테이블 메타데이터 JSON&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;지원 Transform&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Hive-style: 사실상 Identity만 가능(bucket, hour는 별도 컬럼 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Iceberg Hidden: year/month/day/hour, bucket, truncate, hash 등 풍부&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파티션 추가/삭제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Hive-style: ALTER TABLE ADD/DROP PARTITION 반복 &amp;rarr; HMS 락&amp;middot;트래픽 증가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Iceberg Hidden: 파일만 쓰면 끝 &amp;middot; HMS 호출 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿼리 필터&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Hive-style: 사용자 쿼리에 dt='...' 등 파티션 컬럼 명시 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Iceberg Hidden: 원본 컬럼 조건만 써도 자동 프루닝&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파티션 개수 확장성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Hive-style: 수십만 개 넘어가면 HMS&amp;middot;NameNode 병목&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Iceberg Hidden: 파티션 수가 메타데이터 파일 크기에만 비례&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;스키마/파티션 진화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Hive-style: 파티션 컬럼 추가&amp;middot;변경 시 디렉터리 재작성 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Iceberg Hidden: ALTER TABLE ADD PARTITION FIELD &amp;rarr; 과거 스냅샷과 공존 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;카탈로그&amp;middot;쿼리 엔진에 미치는 영향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메타스토어 부하 감소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Hive 방식은 파티션별 행이 HMS DB에 저장되어 매니저블 오브젝트 수가 기하급수적으로 늘어남.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Iceberg는 변동 없는 &amp;ldquo;테이블 한 행&amp;rdquo;만 사용해 DB 락&amp;middot;GC&amp;middot;스케일 문제를 제거함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;계획 시간 일정&amp;middot;I/O 절감&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Iceberg는 매니페스트 통계로 계획을 세우므로 수억 파일이어도 수 KB~MB 메타데이터만 읽음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Hive는 파티션 경로&amp;rarr;파일시스템 LIST 호출이 누적돼 PT 시간&amp;middot;S3 비용이 커짐.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;쿼리 단순화 &amp;amp; 범용성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자&amp;middot;BI 툴은 파티션 컬럼을 몰라도 되므로 오프셋&amp;middot;윈도 함수 같은 복잡한 쿼리를 그대로 작성할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Transform이 추상화돼 있어 Spark&amp;middot;Trino&amp;middot;Flink 등 다양한 엔진이 동일 파티션 레이아웃을 이해함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일부 엔진(Dremio 등)도 &amp;ldquo;Iceberg Partition Transform 지원&amp;rdquo;을 호환성 체크 항목으로 삼는 추세임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;파티션 진화 &amp;amp; 스큐 완화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들어 초기에는 days(ts)였다가 트래픽 급증 구간에만 hours(ts)로 세분화하는 Partition Evolution을 수행해도, 과거/미래 데이터가 한 쿼리에서 매끄럽게 공존함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스키마에서 파티션 컬럼이 빠져 있으므로 코드&amp;middot;쿼리 수정도 최소화됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;&lt;b&gt;설계 시점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;ldquo;사용자가 WHERE 절에 어떤 컬럼을 가장 많이 거는가?&amp;rdquo;를 기준으로 Transform을 선택하면 감춰진 채로도 최적 프루닝이 가능함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;운영 지표&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;매니페스트 크기(GB), 파티션 필드별 카디널리티를 모니터링해 Transform 추가&amp;middot;변경 타이밍을 결정함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Migrate 전략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Hive 테이블을 Iceberg로 변환할 때 IDENTITY(dt) 식부터 매핑하고, 이후 HOUR(ts)&amp;middot;BUCKET(id) 등을 ADD PARTITION FIELD 방식으로 단계적 도입하면 서비스 중단 없이 개선 가능함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Iceberg의 숨은 파티션 메커니즘은 &amp;ldquo;디렉터리 기반 파티션 ≒ 메타스토어 병목&amp;rdquo;이라는 기존 레이크 문제를 근본적으로 해결하면서, 더 풍부한 Transform&amp;middot;진화 기능까지 제공함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 결과 카탈로그는 작고 단순해지고, 쿼리 엔진은 더 똑똑하고 빠른 파티션 프루닝을 실현하게 됨.&lt;/p&gt;</description>
      <category>Cloud/AWS</category>
      <category>AWS</category>
      <author>박경태</author>
      <guid isPermaLink="true">https://pinggoopark.tistory.com/1101</guid>
      <comments>https://pinggoopark.tistory.com/entry/AWS-Partition-Transform%EC%9D%B4-%EA%B0%90%EC%B6%B0%EC%A7%84-%ED%8C%8C%ED%8B%B0%EC%85%98%EC%9D%84-%EA%B0%80%EB%8A%A5%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98#entry1101comment</comments>
      <pubDate>Sat, 17 May 2025 14:10:02 +0900</pubDate>
    </item>
  </channel>
</rss>