<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>플랫폼 개발팀 기술 블로그</title>
    <link>https://team-platform.tistory.com/</link>
    <description>게임 플랫폼 개발자들이 만들어 가는 기술 블로그 입니다.
자신의 개발 역량을 보다 견고히 하고 널리 개발자를 이롭게 하기 위해 차곡차곡 만들어 나갈 것입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 15:36:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>박종명</managingEditor>
    <item>
      <title>Slack File Upload API 발송 횟수 제한(Rate Limit) 해결하기</title>
      <link>https://team-platform.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;슬랙에는 여러가지 방식으로 다른 서비스로부터의 외부 호출을 제공하는데, &lt;b&gt;API Call&lt;/b&gt;을 통한 파일 업로드, 메세지 발송과, 심플하게 메시지 전송을 호출할 수 있는 &lt;b&gt;WebHook&lt;/b&gt; 등을 제공하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우리도 &lt;span style=&quot;color: #333333;&quot;&gt;각종 알림용도로 여러 서비스에 연동해서 사용하는데, &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;(&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;메일 알림.. 장애 알림.. 등)&lt;/i&gt;&lt;/span&gt; 요청이 많지 않던 그 동안은 별 무리 없이 사용 하던 중 몇가지 제한사항이 발생했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Slack API 자체에서 일정 발송 횟수 이상이 넘어갈 경우 &lt;b&gt;Too Many Request(429)&lt;/b&gt; 오류를 내뱉으며 잠시간 &lt;b&gt;Block&lt;/b&gt; 상태가 되버려 요청들이 다 막히게 된다. 일반 메시지의 경우 여러개의 WebHook을 번갈아 가며 사용&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;(WebHook 1개당 초당 1회 제한)&lt;/i&gt;&lt;/span&gt;하는 방식으로 어느정도 잦은 요청도 극복이 가능했지만, File upload의 경우 별도의 API로만 제공되어있어 제한을 피할 방법이 필요해졌다. 이에 &lt;b&gt;429&lt;/b&gt; 오류 발생 시 일정 시간 이후 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;재시도 등의 처리로 극복하려 했으나, 일부 서비스에서 점점 더 잦은 요청으로 파일 메시지의 누락 등이 발생해서 다른 방안이 필요해졌다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우선 Slack이 제공하는 &lt;b&gt;File upload API&lt;/b&gt;의 호출 제한 사항을 보자&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;file.upload.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;551&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQLa7x/btqNXddCQbL/OtB8WMkLHBbz7BNYZECMqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQLa7x/btqNXddCQbL/OtB8WMkLHBbz7BNYZECMqk/img.png&quot; data-alt=&quot;Slack files.upload API의 사양 (출처 : https://api.slack.com/methods/files.upload)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQLa7x/btqNXddCQbL/OtB8WMkLHBbz7BNYZECMqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQLa7x%2FbtqNXddCQbL%2FOtB8WMkLHBbz7BNYZECMqk%2Fimg.png&quot; data-filename=&quot;file.upload.png&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;551&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Slack files.upload API의 사양 (출처 : https://api.slack.com/methods/files.upload)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;POST 방식으로 두가지 방식을 제공하는데, 실질적인 파일을 전송하는 방식은 &lt;b&gt;multipart/form-data&lt;/b&gt;를 사용한다. &lt;i&gt;(다른 방식의 경우 사이즈가 큰 본문이 파일처럼 전송된다.)&amp;nbsp;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;여기서 문제가 되었던 중요 사항인 &lt;b&gt;&lt;i&gt;Rate limiting&lt;/i&gt;&lt;/b&gt;을 보면 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;i&gt;Tier 2 &lt;/i&gt;&lt;/span&gt;로 되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;645&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UAeGb/btqN01DhAmC/ZNJkKCsv9YKMLorcbwQYMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UAeGb/btqN01DhAmC/ZNJkKCsv9YKMLorcbwQYMk/img.png&quot; data-alt=&quot;&amp;amp;amp;nbsp;Tier 2와 다른 일부 Tier의 전송 제한(출처 : https://api.slack.com/docs/rate-limit)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UAeGb/btqN01DhAmC/ZNJkKCsv9YKMLorcbwQYMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUAeGb%2FbtqN01DhAmC%2FZNJkKCsv9YKMLorcbwQYMk%2Fimg.png&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;967&quot; data-origin-height=&quot;645&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;nbsp;Tier 2와 다른 일부 Tier의 전송 제한(출처 : https://api.slack.com/docs/rate-limit)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;20+ per minute&lt;/b&gt;로 되어있다.&lt;b&gt; Note&lt;/b&gt;를&amp;nbsp;보면 1분당 20회, &lt;b&gt;약 3초당 1회&lt;/b&gt; 정도의 요청을 기본적으로 허용하고 일부 불가피하게 넘는 요청도 봐준다(?) 정도로 이해하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우리의 서비스에서 해당 수치는 사실 파일 업로드를 사용하는 부서에서 전송하는 현황으로 생각하면 넉넉한 수치라고 생각했다. 다만 가끔 초당 수십회 이상 파일 업로드가 필요한 그런 경우가 생기게 되었고 이런 상황은 1분당 20회로는 하루에도 여러번 &lt;b&gt;TooManyRequest(429) &lt;/b&gt;와 맞닥뜨릴 수밖에 없는 상황이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이에 처음에는 별도의 파일 서버를 두고 해당 파일의 링크만 담아, 전송 제한으로부터 훨씬 여유로운 &lt;b&gt;WebHook&lt;/b&gt;을 이용하려 했다. 하지만 내부 논의 끝에 새로운 방식을 고안해냈다.&lt;span style=&quot;color: #f89009;&quot;&gt; (&lt;i&gt;논의는 참가하지 않아서 선택기준은 자세히 전달 듣지 못했지만.. 아마 내부서비스이기도 하고 추가 파일서버의 필요함, 파일 전송 및 다운로드간 리소스 소모 등등의 이유였지 않을까 싶다.&lt;/i&gt;)&lt;/span&gt; 새로운 방식은 늦게 메시지를 받아보더라도 수신률은 최대로 유지하는 걸 기본 전략으로 삼아 &lt;b&gt;Queue&lt;/b&gt;에 요청을 쌓아서 3초에 한개씩 전송하고, 오류나 실패시에도 다시 &lt;b&gt;Queue&lt;/b&gt;에 적재하여 재시도하는 방식으로 신뢰성을 유지하는 방식을 선택하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이에, &lt;b&gt;Queue&lt;/b&gt; 역할을 할 &lt;b&gt;Redis&lt;/b&gt;와 알림 API 서비스 내부에 &lt;b&gt;Scheduler&lt;/b&gt;를 구현하기로 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #f89009;&quot;&gt;&lt;i&gt;(Redis는 RabbitMQ 등으로 대체해도 된다.)&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;&lt;b&gt;기존 방식&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;316&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bopdQG/btqN00LiKbY/axLoVBfTaS4OKbQR0dZNzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bopdQG/btqN00LiKbY/axLoVBfTaS4OKbQR0dZNzK/img.png&quot; data-alt=&quot;일반적인 API 호출 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bopdQG/btqN00LiKbY/axLoVBfTaS4OKbQR0dZNzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbopdQG%2FbtqN00LiKbY%2FaxLoVBfTaS4OKbQR0dZNzK%2Fimg.png&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;316&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;일반적인 API 호출 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기존에는 요청을 받으면 알림 서비스가 슬랙 API를 호출하는 간단한 구조였다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;&lt;b&gt;새로운 방식&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;445&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A3IfR/btqNWhODl6t/lXvJuMe6o1qKbUbmnRD2e1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A3IfR/btqNWhODl6t/lXvJuMe6o1qKbUbmnRD2e1/img.png&quot; data-alt=&quot;새로 Redis와 Scheduler를 적용한 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A3IfR/btqNWhODl6t/lXvJuMe6o1qKbUbmnRD2e1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA3IfR%2FbtqNWhODl6t%2FlXvJuMe6o1qKbUbmnRD2e1%2Fimg.png&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;445&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;새로 Redis와 Scheduler를 적용한 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;새로운 방식을 표현한 구조다. 기존 방식과 다르게 &lt;b&gt;Queue&lt;/b&gt;역할을 해주는 &lt;b&gt;Redis&lt;/b&gt;와, 주기적으로 &lt;b&gt;Redis&lt;/b&gt;로부터 전송 데이터를 받아오는 &lt;b&gt;Scheduler&lt;/b&gt;가 추가되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;간단하게 플로우를 설명하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;1. 사용자가 파일을 알림 서비스로 업로드한다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;2. 알림 서비스는 파일을 받아 &lt;b&gt;로컬 서버에 저장&lt;/b&gt;하고&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;3. 그 파일 정보와 전송정보를 &lt;b&gt;Redis&lt;/b&gt;에 적재한다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #f89009; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;(여기까지가 사용자가 실질적으로 전송 요청을 마무리하는 단계)&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;4. 사용자 응답&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;5 알림 서비스의 &lt;b&gt;스케쥴러&lt;/b&gt;가 3초마다 돌며 &lt;b&gt;Redis&lt;/b&gt;에 파일 전송 정보가 있는지 체크하고 가져온다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;6. 전송 정보가 있을 경우 &lt;b&gt;Slack File Upload API&lt;/b&gt;를 호출한다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;7. &lt;b&gt;Slack&lt;/b&gt;으로부터 파일 업로드 결과를 받는다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666; font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;8. 오류가 발생하는 등, 전송에 실패하면 재시도를 위해 &lt;b&gt;Redis&lt;/b&gt;에 다시 정보를 적재한다.&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;API의 기본 제한인 3초 1회 룰을 정확히 지키다 보니, 더이상 &lt;b&gt;429&lt;/b&gt; 오류는 발생하지 않았다. 다만 약간의 burst한 요청을 Slack API가 허용하는 만큼, 파일 업로드 API를 &lt;span style=&quot;color: #333333;&quot;&gt;집중 포화 할 일이 없는 부서가 해당 기능을 사용 할 때의 부득이한 지연을 방지하기 위해서 &lt;/span&gt;내부에서 &lt;u&gt;&lt;b&gt;429&lt;/b&gt;를 발생시키는 잦은 요청과, 일반적인 파일 업로드 요청을 두개의 API로 분리&lt;/u&gt;하기로 했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결과적으로 일반 파일 업로드 요청과 지연 파일 업로드 요청으로 나뉘었는데, 실제 우리 서비스에 적용된 상세 플로우는 다음과 같다&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1331&quot; data-origin-height=&quot;860&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzkX9G/btqNXFBnPg0/6pQkns0wEYb2HySNKHMLEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzkX9G/btqNXFBnPg0/6pQkns0wEYb2HySNKHMLEK/img.png&quot; data-alt=&quot;2개로 나누어진 API의 상세 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzkX9G/btqNXFBnPg0/6pQkns0wEYb2HySNKHMLEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzkX9G%2FbtqNXFBnPg0%2F6pQkns0wEYb2HySNKHMLEK%2Fimg.png&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1331&quot; data-origin-height=&quot;860&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;2개로 나누어진 API의 상세 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1606094247654&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;API : 알림 서비스의 전송 API
Scheduler : 알림 서비스의 일반 파일 업로드를 담당하는 스케쥴러
Scheduler-delayed : 알림 서비스의 지연 파일 업로드를 담당하는 스케쥴러
Sender : 알림 서비스에서 Slack 서버 API를 호출하는 주체
Redis server : Queue 역할을 담당하는 Redis&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;파일 관련 API가 두개로 나뉜 만큼 &lt;b&gt;Scheduler&lt;/b&gt;와 &lt;b&gt;Redis&lt;/b&gt;의 &lt;b&gt;ListKey&lt;/b&gt;도 두개로 늘어났다. &lt;span style=&quot;color: #f89009;&quot;&gt;&lt;i&gt;(실제 서비스에는 일반 메세지를 담당하는 것 까지 3개씩 동작하고 있다)&lt;/i&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;지연 파일 업로드 뿐 아니라, 일반 파일 업로드 기능도 제공하는 만큼 3초당 1회 호출 룰이 약간 초과할 여지가 있기에 &lt;b&gt;429&lt;/b&gt;가 발생할 여지 또한 남아있지만, 기존과 다르게 &lt;b&gt;Queue&lt;/b&gt;형식을 채용함으로서 429가 발생하더라도 약 1분간은 내부에서 글로벌한 static 변수로인해 &lt;span style=&quot;color: #333333;&quot;&gt;Slack &lt;/span&gt;File Upload API 호출을 제한시키면서, &lt;b&gt;Redis&lt;/b&gt;로 전송 관련 정보 적재만 하는 방식을 사용하였고 이후 Slack API에서 제한이 풀릴 쯤 스케쥴러에 의해 자동으로 전송되도록 했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;i&gt;결론&lt;/i&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000; font-family: 'Nanum Gothic';&quot;&gt;전송 횟수 제한이 있는 Slack API를 사용하면서 이에 대한 우회하는 방안을 생각하고 구현하여 적용해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000; font-family: 'Nanum Gothic';&quot;&gt;해당 방식과 동일하게 적용할 경우 3초 1회보다 빠른 요청이 &lt;b&gt;Redis&lt;/b&gt;에 쌓이는 만큼 로컬에 파일이 쌓이는 파일들에 의해 디스크 용량이 부족할 수 있기 때문에 디스크 용량을 넉넉하게 준비해놔야 하는 단점이 있다. 그리고 지연 요청의 경우 많이 쌓인 상태라면, 그 이후 보낸 요청의 경우 늦게 도달하기 때문에 염두할 필요가 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;각 자 본인의 서비스에 맞는 구현 방식으로 변경해서 이용해야 할 것이다.&lt;/span&gt; &lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 조금 더 서비스 규모가 크거나 파일 전송이 잦아진다면, 별도의 파일서버를 이용하고 &lt;b&gt;WebHook&lt;/b&gt; 방식을 사용하여 3초 1회란 제한에서 벗어나, 좀 더 빠르고 안정적으로 대응할 수 있을 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;더 나은 방안이 있다면 언제라도 댓글 주세요 ^^&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Article</category>
      <category>Rate Limit</category>
      <category>slack</category>
      <category>Slack File Upload</category>
      <category>Slack Limit</category>
      <category>Slack Rate Limit</category>
      <category>슬랙</category>
      <category>슬랙 파일업로드</category>
      <category>슬랙 파일업로드 제한</category>
      <author>에스엠에스</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/54</guid>
      <comments>https://team-platform.tistory.com/54#entry54comment</comments>
      <pubDate>Sun, 22 Nov 2020 19:48:55 +0900</pubDate>
    </item>
    <item>
      <title>Scouter Slack Plugin 알림 설정하기</title>
      <link>https://team-platform.tistory.com/53</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;목표&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; 스카우터 슬랙 플러그인을 커스터마이징하여 일부 요청에 대한 알림 여부를 다르게 하고, 요청 설정 목록을 서버 중단 없이 갱신하는 기능을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size14&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* 스카우터에 대한 자세한 설명은 블로그 내 다른 포스트인 &lt;a href=&quot;https://team-platform.tistory.com/14&quot;&gt;https://team-platform.tistory.com/14&lt;/a&gt; 에서 다룬다&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우리는&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; 개발되어 서비스 중인 자바 프로세스 들을 스카우터를 통해 관리하고 있다. 또한 프로세스들의 장애 알림 처리는 스카우터의 빌트인 플러그인인 슬랙(&lt;i&gt;&lt;b&gt;scouter-plugin-server-slack&lt;/b&gt;&lt;/i&gt;) 플러그인을 통해 수행하고있다. 이를 통해 스카우터&amp;nbsp;&lt;i&gt;&lt;b&gt;Xlog&lt;/b&gt;&lt;/i&gt;에서 에러로 감지된 요청이나 응답이 오래 걸린 요청들은 슬랙을 통해 알림을 받고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;근데 그래서 왜?&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; 우리가 운영하는 여러 서비스들의 다양한 요청들 중, 일부의 경우 에러 알림을 굳이 받지 않아야 하는 경우가 있었다. 또한 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;느린 응답을 가진 요청의 경우에도 슬랙 플러그인 설정 값인 &lt;i&gt;&lt;b&gt;ext_plugin_elapsed_time_threshold&lt;/b&gt;&lt;/i&gt; 값으로 글로벌하게만 관리되기 때문에, 자체 서비스 요청 응답이 해당 설정 값을 초과할 수밖에 없는 복잡한 로직을 가지거나 수 ms 이내로 빠르게 응답해야만 하는 서비스들의 경우 장애 여부를 판단하기가 곤란했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;i&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Endpoint&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;로직 수행 시간&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;글로벌 설정 지연 한계값&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;슬랙 장애 알림 여부&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;GET&lt;/b&gt; /users:bulk&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;30,000 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;5,000 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;&lt;b&gt;GET&lt;/b&gt; /users/{userID}&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;10 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;5,000 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #5733b1;&quot;&gt;&lt;i&gt;&amp;nbsp;&lt;span style=&quot;color: #1a5490;&quot;&gt;위 예시에 따르면 첫번째 요청(/users:bulk)의 경우 기본 로직&amp;nbsp; 수행 시간이 길어, 요청할 때마다 슬랙은 느린 응답 장애를 발송할 것이다. 또한 두 번째 요청(/users/{userID})의 경우 1초만 걸려도 장애로 판단되어야 할 경우 글로벌 설정 5초에 의해 느린 응답 알림을 받을 수 없다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 플러그인을 수정하여 요청별로 각각 지연, 또는 장애에 대한 알림이 발생되더라도 발송 자체를 무시하거나 다른 지연 설정값을 적용받을 수 있도록 하는 시스템 개선이 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size14&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;* 슬랙 플러그인 적용에 대한 자세한 사항은 &lt;a href=&quot;https://github.com/scouter-contrib/scouter-plugin-server-alert-slack/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/scouter-contrib/scouter-plugin-server-alert-slack/&lt;/a&gt; 에서 확인하자&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;수정한 부분&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당문제를 해결하기 위해선 &lt;i&gt;&lt;b&gt;SlackPlugin.java&lt;/b&gt;&lt;/i&gt; 파일의 &lt;span style=&quot;color: #333333;&quot;&gt;&lt;i&gt;&lt;b&gt;@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)&lt;/b&gt;&lt;/i&gt; 어노테이션이 걸린 메서드를 수정해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;원본 SlackPlugin.java&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1591161384466&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)
    public void xlog(XLogPack pack) {

        String objType = AgentManager.getAgent(pack.objHash).objType;

        if (groupConf.getBoolean(&quot;ext_plugin_slack_xlog_enabled&quot;, objType, true)) {
            if (pack.error != 0) {
                String date = DateUtil.yyyymmdd(pack.endTime);
                String service = TextRD.getString(date, TextTypes.SERVICE, pack.service);
                AlertPack ap = new AlertPack();
                ap.level = AlertLevel.ERROR;
                ap.objHash = pack.objHash;
                ap.title = &quot;xlog Error&quot;;
                ap.message = service + &quot; - &quot; + TextRD.getString(date, TextTypes.ERROR, pack.error);
                ap.time = System.currentTimeMillis();
                ap.objType = objType;
                alert(ap);
            }

            try {
                int elapsedThreshold = groupConf.getInt(&quot;ext_plugin_elapsed_time_threshold&quot;, objType, 0);

                if (elapsedThreshold != 0 &amp;amp;&amp;amp; pack.elapsed &amp;gt; elapsedThreshold) {
                    String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE,
                            pack.service);

                    AlertPack ap = new AlertPack();

                    ap.level = AlertLevel.WARN;
                    ap.objHash = pack.objHash;
                    ap.title = &quot;Elapsed time exceed a threshold.&quot;;
                    ap.message = &quot;[&quot; + AgentManager.getAgentName(pack.objHash) + &quot;] &quot; + pack.service + &quot;(&quot; + serviceName
                            + &quot;) &quot; + &quot;elapsed time(&quot; + pack.elapsed + &quot; ms) exceed a threshold.&quot;;
                    ap.time = System.currentTimeMillis();
                    ap.objType = objType;

                    alert(ap);
                }

            } catch (Exception e) {
                Logger.printStackTrace(e);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당 메서드는 &lt;i&gt;Xlog&lt;/i&gt;의 요청이 기록 됐을 때 진입되는 부분인데 &lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이곳을 보면 에러에 대한 알림을 보내는 영역, 느린 응답에 대한 알림을 보내는 영역이 있는 걸 알 수 있다. 간단하게 슬랙 알림을 무시할 필요가 있는 응답을 추가해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;수정 SlackPlugin.java&lt;/b&gt;&lt;/i&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1591161376705&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)
    public void xlog(XLogPack pack) {

        String objType = AgentManager.getAgent(pack.objHash).objType;

        if (groupConf.getBoolean(&quot;ext_plugin_slack_xlog_enabled&quot;, objType, true)) {
            String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE, pack.service);
            String agentName = AgentManager.getAgentName(pack.objHash);
            if (pack.error != 0) {
                if (&quot;localhost&quot;.equals(agentName)) {
                    // 특정 Agent는 에러 무시
                } else if (&quot;/test&quot;.equals(serviceName)) {
                    // /test 요청은 에러 무시
                } else {
                    // 그 외에는 에러 발송
                    String date = DateUtil.yyyymmdd(pack.endTime);
                    String service = TextRD.getString(date, TextTypes.SERVICE, pack.service);
                    AlertPack ap = new AlertPack();
                    ap.level = AlertLevel.ERROR;
                    ap.objHash = pack.objHash;
                    ap.title = &quot;xlog Error&quot;;
                    ap.message = service + &quot; - &quot; + TextRD.getString(date, TextTypes.ERROR, pack.error);
                    ap.time = System.currentTimeMillis();
                    ap.objType = objType;
                    alert(ap);
                }
            }

            try {
                int elapsedThreshold = groupConf.getInt(&quot;ext_plugin_elapsed_time_threshold&quot;, objType, 0);
				boolean slowError = true;
                if (elapsedThreshold != 0 &amp;amp;&amp;amp; pack.elapsed &amp;lt; 1000
                        &amp;amp;&amp;amp; &quot;/users&quot;.equals(serviceName)) {
                        // 1초만 지나도 에러인 응답 체크
                	slowError = false;
                } else if (elapsedThreshold != 0 &amp;amp;&amp;amp; pack.elapsed &amp;gt; elapsedThreshold) {
                    if (pack.elapsed &amp;lt; 60000 &amp;amp;&amp;amp; &quot;/users:bulk&quot;.equals(serviceName)
                    	&amp;amp;&amp;amp; &quot;/gateway-service&quot;.equals(agentName)) {
                        // /users:bulk 요청은 60초가 넘어야 에러 발송
                        slowError = false;
                    }
                    ... more
                }
                
                if( slowError ) {
                  AlertPack ap = new AlertPack();

                  ap.level = AlertLevel.WARN;
                  ap.objHash = pack.objHash;
                  ap.title = &quot;Elapsed time exceed a threshold.&quot;;
                  ap.message = &quot;[&quot; + agentName + &quot;] &quot; + pack.service + &quot;(&quot; + serviceName + &quot;) &quot; + &quot;elapsed time(&quot;
                                  + pack.elapsed + &quot; ms) exceed a threshold.&quot;;
                  ap.time = System.currentTimeMillis();
                  ap.objType = objType;

                  alert(ap);
                }

            } catch (Exception e) {
                Logger.printStackTrace(e);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위 처럼 수정 할 경우 /test 요청은 에러가 발생해도 무시되고, /users 는 1초만 지연되더라도, /users:bulk는 60초가 넘어야만 느린 응답 알림이 발생될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;와 됐다!.. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;됐나..?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;진짜 문제는..&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;&lt;b&gt;Slack-Plugin&lt;/b&gt;&lt;/i&gt;은 빌트인 플러그인인 만큼 수정할 경우 스카우터 서버를 재기동해야 하는 단점이 있었다. 또한 하드코딩으로 관리되는 장애 알림 무시 대상 목록의 경우 &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음에는 몇 개 되지 않아 관리에 불편함이 없었지만, 지속적으로 개발됨에 따라 점점 개수가 늘어나고 수정 또한 빈번하게 발생되어 관리를 할 필요성이 생겨 다음과 같은 관점에서 변경을 고민해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;square&quot;&gt;
&lt;li&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;알림 무시 대상의 목록을 수정하면 서버를 재기동하지 않더라도 바로 적용이 되어야 한다&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;기존 하드코딩으로 구현되었던 조건문을 수용할 수 있도록 간단한 로직이 허용되어야 한다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;알림 무시 대상 목록을 수정할 때 서버 소스의 변경은 최소화해야 한다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;바꾸자&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터베이스로 알림 무시 대상 목록을 따로 관리하는 방법도 있겠지만, 따로 데이터베이스를 구성해야 하고 jdbc도 직접 구현하거나 다른 라이브러리를 첨부해야 하기에 조금 더 쉬운 방법으로 접근했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;먼저 슬랙 플러그인 소스 중 &lt;i&gt;&lt;b&gt;SlackPlugin.java&lt;/b&gt;&lt;/i&gt;에서 스카우터 관련 설정 파일을 읽어 들이는 방식과 같은 방식으로 파일 기반으로 알림 무시 대상 목록을 관리하고자 했다. &lt;i&gt;&lt;b&gt;scouter&lt;/b&gt; &lt;/i&gt;패키지에 속해있는 &lt;i&gt;&lt;b&gt;Configure.java&lt;/b&gt; &lt;/i&gt;파일을 보면 3초마다 지정된 경로에 있는 설정 파일을 주기적으로 &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;체크하면서&amp;nbsp;수정사항이&amp;nbsp;있을&amp;nbsp;경우&amp;nbsp;서버&amp;nbsp;메모리&amp;nbsp;&lt;i&gt;&lt;b&gt;Properties&amp;nbsp;&lt;/b&gt;&lt;/i&gt;객체에 패치하는 형태로 구성되어있다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이와 같은 파일 갱신 프로세스를 알림 무시 대상 목록에도 적용하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;SkipConditionalConfigure.java&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1591161351778&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package scouter.plugin.server.alert.slack.skipconfig;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import scouter.util.FileUtil;
import scouter.util.StringUtil;
import scouter.util.ThreadUtil;

public class SkipConditionalConfigure extends Thread {

    private static SkipConditionalConfigure instance = null;
    public final static String CONDITIONAL_FILE_LOC = &quot;./conditional/&quot;;
    private List&amp;lt;SkipConditional&amp;gt; errorSkipConditional = new ArrayList&amp;lt;SkipConditional&amp;gt;();
    private List&amp;lt;SkipConditional&amp;gt; slowSkipConditional = new ArrayList&amp;lt;SkipConditional&amp;gt;();

    public List&amp;lt;SkipConditional&amp;gt; getErrorSkipConditional() {
        return errorSkipConditional;
    }

    public List&amp;lt;SkipConditional&amp;gt; getSlowSkipConditional() {
        return slowSkipConditional;
    }

    public final static synchronized SkipConditionalConfigure getInstance() {
        if (instance == null) {
            instance = new SkipConditionalConfigure();
            instance.setDaemon(true);
            instance.setName(ThreadUtil.getName(instance));
            instance.start();
        }
        return instance;
    }

    private SkipConditionalConfigure() {
        reload(false);
    }

    private boolean isModify = false;

    public boolean isModified() {
        return this.isModify;
    }

    private long last_load_time = -1;
    public Properties property = new Properties();

    private boolean running = true;

    public void run() {
        while (running) {
            reload(false);
            ThreadUtil.sleep(3000);
        }
    }

    private File conditionalFile;

    public File getConditionalFile() throws Exception {
        if (conditionalFile != null) {
            return conditionalFile;
        }
        String s = System.getProperty(&quot;scouter.conditional&quot;, CONDITIONAL_FILE_LOC + &quot;conditional.conf&quot;);
        conditionalFile = new File(s.trim());
        return conditionalFile;
    }

    long last_check = 0;

    public synchronized boolean reload(boolean force) {
        long now = System.currentTimeMillis();
        if (force == false &amp;amp;&amp;amp; now &amp;lt; last_check + 3000) {
            return false;
        }
        last_check = now;

        File file = null;
        try {
            file = getConditionalFile();
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        isModify = !(file.lastModified() == last_load_time);
        if (!isModify) {
            return false;
        } else {
            System.out.println(&quot;File Modified&quot;);
        }

        last_load_time = file.lastModified();

        Properties temp = new Properties();
        if (file.canRead()) {
            FileInputStream in = null;
            try {
                in = new FileInputStream(file);
                temp.load(in);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                FileUtil.close(in);
            }
        }
        property = replaceSysProp(temp);

        setErrorSkipConditionals();
        setSlowSkipConditionals();
        return true;
    }

    public Properties replaceSysProp(Properties temp) {
        Properties p = new Properties();

        Map&amp;lt;Object, Object&amp;gt; args = new HashMap&amp;lt;Object, Object&amp;gt;();
        args.putAll(System.getenv());
        args.putAll(System.getProperties());

        p.putAll(args);

        Iterator&amp;lt;Object&amp;gt; itr = temp.keySet().iterator();
        while (itr.hasNext()) {
            String key = (String) itr.next();
            String value = (String) temp.get(key);
            p.put(key, new scouter.util.ParamText(StringUtil.trim(value)).getText(args));
        }

        return p;
    }

    private void setErrorSkipConditionals() {
        errorSkipConditional.clear();
        int lineNo = 1;
        while (true) {
            try {
                String originStr = getValue(&quot;error_alert_skip_conditional_&quot; + lineNo);
                if (originStr == null || &quot;&quot;.equals(originStr)) {
                    break;
                }
                SkipConditional conditional = parseSkipConditional(originStr);
                if (conditional == null) {
                    System.out.println(&quot;fail skip conditional parse.&quot;);
                    break;
                }
                errorSkipConditional.add(conditional);
                lineNo++;
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
    private SkipConditional parseSkipConditional(String originStr) {
        String[] p1 = originStr.split(&quot;\\::&quot;);
        if (p1.length == 0) {
            System.out.println(&quot;[p1] split length error&quot;);
            return null;
        }
        SkipConditional conditional = new SkipConditional();
        String p2;
        if (p1.length &amp;gt; 1) {
            conditional.setElapsedTime(Long.parseLong(p1[0]));
            p2 = p1[1];
        } else {
            conditional.setElapsedTime(0L);
            p2 = p1[0];
        }
        String[] p3 = { p2 };
        if (p2.contains(&quot;&amp;amp;&amp;amp;&quot;)) {
            p3 = p2.split(&quot;\\&amp;amp;\\&amp;amp;&quot;);
        } 

        if (p3 == null || p3.length == 0) {
            System.out.println(&quot;[p3] split length error&quot;);
            return null;
        }
        String[] p4;
        String value = null;
        TargetType targetType = null;
        CompareType compareType = null;

        List&amp;lt;ValueConditional&amp;gt; valueConditionals = new ArrayList&amp;lt;ValueConditional&amp;gt;();
        for (int i = 0; i &amp;lt; p3.length; i++) {
            String vc = StringUtil.trim(p3[i]);
            p4 = vc.split(&quot;\\,&quot;);
            value = p4[0];
            targetType = TargetType.valueOf(p4[1]);
            compareType = CompareType.valueOf(p4[2]);
            valueConditionals.add(new ValueConditional(value, targetType, compareType));
        }
        conditional.setValueConditionals(valueConditionals);

        return conditional;
    }

    private void setSlowSkipConditionals() {
        slowSkipConditional.clear();
        int no = 1;
        while (true) {
            try {
                String originStr = getValue(&quot;slow_alert_skip_conditional_&quot; + no);
                if (originStr == null || &quot;&quot;.equals(originStr)) {
                    break;
                }
                SkipConditional conditional = parseSkipConditional(originStr);
                if (conditional == null) {
                    System.out.println(&quot;fail skip conditional parse.&quot;);
                    break;
                }
                slowSkipConditional.add(conditional);
                no++;
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }

    public List&amp;lt;String&amp;gt; getValueList(String key) {
        String str = StringUtil.trim(property.getProperty(key));
        if (str == null) {
            return Collections.emptyList();
        } else {
            return Arrays.asList(str.split(&quot;\\,&quot;));
        }
    }

    public String getValue(String key) {
        return StringUtil.trim(property.getProperty(key));
    }

    public String getValue(String key, String def) {
        return StringUtil.trim(property.getProperty(key, def));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;구성은 &lt;b&gt;&lt;i&gt;Configure.class&lt;/i&gt;&lt;/b&gt;파일과 거의 유사하고, 파일을 갱신하는 핵심 부분의 로직만 옮겨왔다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;추가로 두 개의 알림을 관리하는 목록 클래스로서 &lt;b&gt;&lt;i&gt;errorSkipConditional,&lt;/i&gt; slowSkipConditional&lt;/b&gt;를 생성, 갱신하는 로직이 추가되어있다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;굳이 로직의 동작을 설명하자면 매 3초마다 지정된 경로(&lt;i&gt;/conditional&lt;/i&gt;)의 지정된 파일(&lt;i&gt;conditional.conf&lt;/i&gt;)을 읽어 들여, 해당 파일의 수정 시간과 저장된 수정 시간을 비교하여 변경됐을 경우 기존 목록 클래스를 초기화하고 다시 읽어 들인 파일로부터 파싱 과정을 거쳐 목록 클래스를 갱신하는 형태로 구성되어있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;읽어 들이는 설정 파일의 경우 기존 하드코딩된 로직을 수용할 수 있도록 조건들을 포함해서 구성했다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;i&gt;UrlPattern &lt;/i&gt;방식을 사용하거나, 일치(equals) 체크 -&amp;gt; 포함(contains) 체크 순으로 검증하는 방식도 생각해 봤지만 기존 하드코딩되어있던 조건의 경우 일치하거나 포함하는 조건으로만 되어있었기 때문에 우선적으로 기존 코드와 맞게 일치, 포함 여부를 명세하는 조건 방식으로 구성하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;i&gt;&lt;b&gt;/conditional/conditional.conf&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1591156450871&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 에러 알림 무시 목록
# {value},{AGENT|SERVICE},{MATCH|CONTAINS} {&amp;amp;&amp;amp; (optionals)} {other conditional (optionals)}
error_alert_skip_conditional_1=/test,AGENT,MATCH
error_alert_skip_conditional_2=/actuator,AGENT,MATCH
error_alert_skip_conditional_3=/error,AGENT,MATCH

# 느린 응답 알림 무시 목록
# {skipElapsedTime}::{value},{AGENT|SERVICE},{MATCH|CONTAINS} {&amp;amp;&amp;amp;(optionals)} {other conditional (optionals)}
slow_alert_skip_conditional_1=100::/board/1,SERVICE,CONTAINS &amp;amp;&amp;amp; /gateway-service,AGENT,MATCH
slow_alert_skip_conditional_2=10000::/users:all,SERVICE,MATCH
slow_alert_skip_conditional_3=30000::/users:bulk,SERVICE,MATCH&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp; 설정 파일의 경우 일반적인 오류와, 느린 응답에 대한 값들로 구성되어있다. 기본적으로 Xlog에 기록되는 객체 값에서 Service, Agent 텍스트 값을 비교하여 알림 여부를 결정하게 된다. 해당 구성 파일로 가능한 것은 &lt;i&gt;특정 &lt;b&gt;&lt;span style=&quot;color: #8cb3be;&quot;&gt;지연 시간 설정 이내&lt;/span&gt;&lt;/b&gt;의, &lt;b&gt;&lt;span style=&quot;color: #8cb3be;&quot;&gt;SERVICE &lt;/span&gt;&lt;/b&gt;또는 &lt;b&gt;&lt;span style=&quot;color: #8cb3be;&quot;&gt;AGENT&lt;/span&gt;&lt;/b&gt;의 값이 &lt;span style=&quot;color: #8cb3be;&quot;&gt;&lt;b&gt;일치(MATCH)&lt;/b&gt;&lt;/span&gt;하거나, 값을 &lt;b&gt;&lt;span style=&quot;color: #8cb3be;&quot;&gt;포함(CONTAINS)&lt;/span&gt;&lt;/b&gt;할 경우 알림을 무시한다.&lt;/i&gt; 고 이해하면 된다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span&gt;&amp;nbsp; 기존 &lt;i&gt;&lt;b&gt;Configure.class&lt;/b&gt;&lt;/i&gt;를 그대로 모방하다 보니, &lt;i&gt;Properties의&lt;/i&gt; 구성 형태처럼 &lt;b&gt;Key / Value&lt;/b&gt; 형태로 관리해야만 해서 설정 파일의 목록 구성이 순차적으로 이루어져야 하는 아쉬운 점이 있었다. 또한 &lt;i&gt;PathVariable &lt;/i&gt;형태의 값의 경우 &lt;i&gt;Regex&lt;/i&gt;나 &lt;i&gt;Pattern&lt;/i&gt;기반으로 검사해야 하는 불편함에 제외하고 값이 포함되는 것으로 체크하도록 하였다. &lt;span style=&quot;color: #666666;&quot;&gt;포함으로 체크할 경우 다른 문제가 있는데, 이는 검사 순서를 변경하거나 &lt;i&gt;Pattern&lt;/i&gt; 기반으로 변경하긴 해야 한다.&lt;/span&gt; &lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;설정된 파일이 주기적으로 &lt;i&gt;&lt;b&gt;SkipConditional&lt;/b&gt;&lt;/i&gt;라고 만들어놓은 객체의 리스트로 갱신되고, 기존 &lt;i&gt;If, else&lt;/i&gt;로 처리했던 &lt;i&gt;&lt;b&gt;SlackPlugin.java &lt;/b&gt;&lt;/i&gt;에서 다음과 같이 비교하도록 처리된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;SkipConditional.java, ValueConditaional.java, Enum&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1591161342291&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SkipConditional {
    private List&amp;lt;ValueConditional&amp;gt; valueConditionals;
    private long elapsedTime;

    public List&amp;lt;ValueConditional&amp;gt; getValueConditionals() {
        return valueConditionals;
    }

    public void setValueConditionals(List&amp;lt;ValueConditional&amp;gt; valueConditionals) {
        this.valueConditionals = valueConditionals;
    }

    public long getElapsedTime() {
        return elapsedTime;
    }

    public void setElapsedTime(long elapsedTime) {
        this.elapsedTime = elapsedTime;
    }

}

public class ValueConditional {
    private String value;
    private TargetType targetType;
    private CompareType compareType;

    public ValueConditional(String value, TargetType targetType, CompareType compareType) {
        this.value = value;
        this.targetType = targetType;
        this.compareType = compareType;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public TargetType getTargetType() {
        return targetType;
    }

    public void setTargetType(TargetType targetType) {
        this.targetType = targetType;
    }

    public CompareType getCompareType() {
        return compareType;
    }

    public void setCompareType(CompareType compareType) {
        this.compareType = compareType;
    }

}

public enum TargetType {
    SERVICE, AGENT
}

public enum CompareType {
    MATCH, CONTAINS
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-size=&quot;size14&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;한 개의 &lt;i&gt;&lt;b&gt;SkipConditional.java&lt;/b&gt;&lt;/i&gt; 는 &lt;i&gt;&lt;b&gt;conditaional.conf&lt;/b&gt;&lt;/i&gt; 파일의 한 &lt;i&gt;row&lt;/i&gt;와 일치하도록 구성되어있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;&lt;b&gt;SlackPlugin.java&lt;/b&gt;의 검사 로직 일부&lt;/i&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1591161335378&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // 최상단에Configure와 같은 방식으로 초기화 해준다.
    final SkipConditionalConfigure skipConf = SkipConditionalConfigure.getInstance();
    
    // 알림 무시 검사 로직
    private boolean skipCheck(String agentName, String serviceName, int elapsed) {
        for (SkipConditional con : skipConf.getSlowSkipConditional()) {
            boolean condition = valueConditionalCheck(con, agentName, serviceName);
            long conpareElapsed = con.getElapsedTime();

            if (conpareElapsed &amp;gt; 0) {
                condition = condition &amp;amp;&amp;amp; elapsed &amp;lt; conpareElapsed;
            }
            if (condition)
                return condition;
        }
        return false;
    }

    private boolean valueConditionalCheck(SkipConditional con, String agentName, String serviceName) {
        Boolean isSkip = null;
        for (ValueConditional vc : con.getValueConditionals()) {
            String value = null;
            String compareValue = vc.getValue();
            switch (vc.getTargetType()) {
            case AGENT:
                value = agentName;
                break;
            case SERVICE:
                value = serviceName;
                break;
            default:
                return false;
            }
            switch (vc.getCompareType()) {
            case MATCH:
                if (isSkip == null) {
                    isSkip = value.equals(compareValue);
                } else {
                    isSkip = isSkip &amp;amp;&amp;amp; value.equals(compareValue);
                }
                break;
            case CONTAINS:
                if (isSkip == null) {
                    isSkip = value.contains(compareValue);
                } else {
                    isSkip = isSkip &amp;amp;&amp;amp; value.contains(compareValue);
                }
                break;
            }
        }
        return isSkip;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;알림 무시 조건을 분석해서 알림 무시 여부를 리턴해주는 간단한 로직이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정 파일의 구성이나 비교 로직은 입맛에 맞게 상황에 맞게 변경해서 쓰면 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;끝 &lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;스카우터 플러그인 수정 작업을 처음 맡아 어려움이 예상됐지만 보면 바로 알 정도로 플러그인이 간편하게 구성되어 있어 수정에도 큰 어려움이 없었다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재는 파일 기반 설정 방식으로 조건이 포함되어 설정 파일 내용이 약간은 난해한 구성인데, 추후 SQLite 같은 small database 형태로 변경해서 다양한 조건들을 직관적으로 구성하여 쉽게 처리할 수 있게 하는 것도 좋을 것 같은 생각이 들었다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Article</category>
      <category>스카우터</category>
      <category>슬랙</category>
      <category>알림무시</category>
      <category>플러그인</category>
      <author>에스엠에스</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/53</guid>
      <comments>https://team-platform.tistory.com/53#entry53comment</comments>
      <pubDate>Wed, 3 Jun 2020 14:27:03 +0900</pubDate>
    </item>
    <item>
      <title>Google AMP 개요 편</title>
      <link>https://team-platform.tistory.com/52</link>
      <description>&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;Google AMP란?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;img01.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cduNTr/btqvDixXbUF/GhvKZ3Klct2eMN1SdYWkj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cduNTr/btqvDixXbUF/GhvKZ3Klct2eMN1SdYWkj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cduNTr/btqvDixXbUF/GhvKZ3Klct2eMN1SdYWkj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcduNTr%2FbtqvDixXbUF%2FGhvKZ3Klct2eMN1SdYWkj1%2Fimg.png&quot; data-filename=&quot;img01.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;AMP는 Accelerated Mobile Pages의 약자로 가속화된 모바일 페이지라고 직역 할 수 있다. Google에서 공개한 오픈소스 라이브러리로 특징으로는&amp;nbsp;&lt;span&gt;정적 콘텐츠의 빠른 렌더링이 가능한 웹페이지를 제작 할 수 있도록 규격화된 기능을 지원한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기존의 웹 페이지 기술을 그대로 사용하고 있고 다양한 브라우저에서 지원되고 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP를 왜 사용하는 것일까?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;웹 사이트의 성능최적화와 CDN을 무료로 사용 할 수 있다.&lt;/li&gt;
&lt;li&gt;AMP가 적용된 웹 사이트는 구글 검색 순위에서 우선적으로 노출 될 수 있다.&lt;/li&gt;
&lt;li&gt;웹 사이트 제작 도구와 다양한 템플릿을 무료로 제공하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;&lt;span&gt;AMP의 주요 기능&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Google AMP는 웹 페이지를 빠른속도로 렌더링 하기위해 html코드를 작성하기 위한 규격이 있다.&amp;nbsp;AMP HTML과 AMP JS라이브러리 Google AMP 캐시를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;AMP HTML :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AMP전용 HTML로 페이지 내의 태그의 형태는 보통의 HTML 코드와 같으나 일부 HTML 태그들은 AMP 전용태그가 사용되고 있다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AMP JS :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AMP JS 라이브러리는 모든 AMP 성능 권장사항을 구현하였고 리소스 로딩을 관리하며 맞춤 태그를 지원하고 있어 빠른 페이지 렌더링을 보장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Google AMP 캐시 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AMP HTML페이지를 가져와 캐시하여 자동으로 페이지 성능을 개선한다. 문서와 모든 JS파일 및 이미지가 하나의 출처에서 로드되므로 효율성이 극대화 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;&lt;span&gt;AMP의 작동 원리&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;AMP 페이지의 렌더링이 빠르게 이루어지는 이유는 AMP가 가지고 있는 일련의 규격에 맞추어 작동하고 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;비동기 스크립트만 허용 :&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;코드에 따라 DOM 구성을 차단하고 페이지 렌더링을 지연시키는 부작용이 있기때문에&lt;/span&gt;&lt;span&gt;&amp;nbsp;규격화 된 커스텀 AMP 요소를 통해 구현해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;모든 리소스의 사이즈를 정적으로 지정 :&amp;nbsp;&lt;/b&gt;&lt;/span&gt;외부 리소스 다운로드 전 HTML 요소들의 사이즈를 지정해주면 리소스 다운로드 여부와 관계없이 레이아웃먼저 로드할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 리소스에 의한 렌더링 차단 방지 :&lt;/b&gt;&amp;nbsp;AMP는 기본적으로 유튜브, 트위터, 인스타그램같은 외부 리소스의 로드를 위해 커스텀 태그를 지원하고 있고 커스텀 태그 사용 전 아래와 같이 스크립트를 로드 해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1559184738720&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script async custom-element=&quot;amp-iframe&quot; src=&quot;https://cdn.ampproject.org/v0/amp-youtube-0.1.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 페이지에서 모든 외부 자바스크립트 제외 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;외부에서 로드되는 자바스크립트는 동기식 로딩이 많아 페이지 로드에 지연을 발생 시킬 수 있다. 때문에 기본 페이지 내의 iframe에서만 외부 자바스크립트를 허용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 CSS는 Inline 방식이며 크기가 한정됨 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;CSS는 모든 렌더링과 페이지 로드를 차단하고 용량이 과도하게 커지는 경향이 있다. 때문에 AMP에서는 CSS의 작성을 인라인 스타일만 허용하고 있고 용량 또한 50KB 이하로 제한하고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율적인 폰트 트리거 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;웹 폰트는 용량이 매우 크기때문에 성능을 위해서는 웹 폰트 최적화가 필수이다. AMP에서는 페이지 로드 시 폰트부터 다운로드 후 자바스크립트와 인라인 스타일 시트가 로드된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스타일 재계산 최소화 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요소의 크기를 지정할 때마다 스타일 재계산이 트리거 되는데 브라우저 페이지에서 전체 페이지를 다시 레이아웃해야 하기때문에 페이지 로드 속도가 느려진다. AMP 페이지에서는 DOM 읽기가 모두 끝난 후에 스타일이 재계산 되므로 프레임마다 최대 한 번만 재계산 되어 성능 저하를 방지 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPU 가속 애니메이션만 실행 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;성능 향상을 위해 레이어 애니메이션은 GPU에서 처리하도록 하고 페이지 레이아웃 업데이트 구간에서는 성능 저하가 발생할 수 있으므로 애니메이션에 관련하여 CSS에 대한 규칙을 지정하고 GPU 가속이 적용되는 애니메이션만 사용하도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우선순위별 리소스 로드 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AMP는 모든 리소스에 대한 다운로드를 제어하며 리소스 로드에 우선순위를 지정하여 필요한 리소스는 로딩하고 바로 로딩할 필요가 없는 리소스는 데이터를 미리 가져와서(prefetches) 로드 대기 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;즉각적인 페이지 로드 :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;AMP는 대역폭과 CPU 사용량을 줄이도록 최적화 되어 있으며 사용자가 명시적으로 이동 의사를 밝히기 전에 목표 페이지를 렌더링해 두었다가 실제 페이지를 선택 할 때 즉시 로드 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP 페이지의 기본구조&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1559184738721&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html ⚡ lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,minimum-scale=1,initial-scale=1&quot;&amp;gt;

    &amp;lt;link rel=&quot;canonical&quot; href=&quot;/article.html&quot;&amp;gt;
    &amp;lt;link rel=&quot;shortcut icon&quot; href=&quot;amp_favicon.png&quot;&amp;gt;

    &amp;lt;title&amp;gt;News Article&amp;lt;/title&amp;gt;

    &amp;lt;style amp-boilerplate&amp;gt;body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}&amp;lt;/style&amp;gt;&amp;lt;noscript&amp;gt;&amp;lt;style amp-boilerplate&amp;gt;body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}&amp;lt;/style&amp;gt;&amp;lt;/noscript&amp;gt;
    &amp;lt;style amp-custom&amp;gt;
      body {
        width: auto;
        margin: 0;
        padding: 0;
      }

      header {
        background: Tomato;
        color: white;
        font-size: 2em;
        text-align: center;
      }

      h1 {
        margin: 0;
        padding: 0.5em;
        background: white;
        box-shadow: 0px 3px 5px grey;
      }

      p {
        padding: 0.5em;
        margin: 0.5em;
      }
    &amp;lt;/style&amp;gt;
    &amp;lt;script async src=&quot;https://cdn.ampproject.org/v0.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script type=&quot;application/ld+json&quot;&amp;gt;
    {
     &quot;@context&quot;: &quot;http://schema.org&quot;,
     &quot;@type&quot;: &quot;NewsArticle&quot;,
     &quot;mainEntityOfPage&quot;:{
       &quot;@type&quot;:&quot;WebPage&quot;,
       &quot;@id&quot;:&quot;https://example.com/my-article.html&quot;
     },
     &quot;headline&quot;: &quot;My First AMP Article&quot;,
     &quot;image&quot;: {
       &quot;@type&quot;: &quot;ImageObject&quot;,
       &quot;url&quot;: &quot;https://example.com/article_thumbnail1.jpg&quot;,
       &quot;height&quot;: 800,
       &quot;width&quot;: 800
     },
     &quot;datePublished&quot;: &quot;2015-02-05T08:00:00+08:00&quot;,
     &quot;dateModified&quot;: &quot;2015-02-05T09:20:00+08:00&quot;,
     &quot;author&quot;: {
       &quot;@type&quot;: &quot;Person&quot;,
       &quot;name&quot;: &quot;John Doe&quot;
     },
     &quot;publisher&quot;: {
       &quot;@type&quot;: &quot;Organization&quot;,
       &quot;name&quot;: &quot;⚡ AMP Times&quot;,
       &quot;logo&quot;: {
         &quot;@type&quot;: &quot;ImageObject&quot;,
         &quot;url&quot;: &quot;https://example.com/amptimes_logo.jpg&quot;,
         &quot;width&quot;: 600,
         &quot;height&quot;: 60
       }
     },
     &quot;description&quot;: &quot;My first experience in an AMPlified world&quot;
    }
    &amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;header&amp;gt;
      News Site
    &amp;lt;/header&amp;gt;
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;Article Name&amp;lt;/h1&amp;gt;

      &amp;lt;p&amp;gt;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam egestas tortor sapien, non tristique ligula accumsan eu.&amp;lt;/p&amp;gt;

      &amp;lt;amp-img src=&quot;mountains.jpg&quot; layout=&quot;responsive&quot; width=&quot;266&quot; height=&quot;150&quot;&amp;gt;&amp;lt;/amp-img&amp;gt;
    &amp;lt;/article&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP 페이지 필수 요소 설명&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP 페이지임을 명시&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;최상위 태그로 &amp;lt;html ⚡ &amp;gt; 또는 &amp;lt;html amp&amp;gt;을 선언하여 AMP 문서로 인식 될 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1559184738722&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html ⚡&amp;gt; 
&amp;lt;!-- 또는 --&amp;gt;
&amp;lt;html amp&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP JS 라이브러리 로드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1559184738722&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script async src=&quot;https://cdn.ampproject.org/v0.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP 보일러플레이트 코드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1559184738722&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style amp-boilerplate&amp;gt;body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}&amp;lt;/style&amp;gt;&amp;lt;noscript&amp;gt;&amp;lt;style amp-boilerplate&amp;gt;body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}&amp;lt;/style&amp;gt;&amp;lt;/noscript&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP 이미지 태그 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1559184738722&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;amp-img src=&quot;mountains.jpg&quot; layout=&quot;responsive&quot; width=&quot;266&quot; height=&quot;150&quot;&amp;gt;&amp;lt;/amp-img&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP에서는 이미지를 추가 하기 위해서는 전용 태그를 사용해야 한다.&lt;/li&gt;
&lt;li&gt;이미지를 추가 할 때 사이즈(width, height)는 반드시 지정해야 한다.&lt;/li&gt;
&lt;li&gt;이미지 표기를 위해 frame, object, param, embed 등 일부 html태그는 AMP에서 사용 할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP 커스텀 CSS 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1559184738722&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style amp-custom&amp;gt;
      body {
        width: auto;
        margin: 0;
        padding: 0;
      }

      header {
        background: Tomato;
        color: white;
        font-size: 2em;
        text-align: center;
      }
&amp;lt;/style&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP 페이지에서 사용하기 위한 커스텀 CSS를 설정한다.&lt;/li&gt;
&lt;li&gt;커스텀 CSS는 &amp;lt;head&amp;gt; 태그 내부에 인라인으로 설정 되어야 한다.&lt;/li&gt;
&lt;li&gt;커스텀 CSS는 최대 50KB를 넘을 수 없다.&lt;/li&gt;
&lt;li&gt;!important를 사용할 수 없으며 외부 스타일 참조가 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP Link 페이지 연결&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP가 적용된 페이지와 적용되어있지 않은 페이지를 둘 다 가진 사이트가 존재하고 Google 검색에서 AMP가 적용되어있지 않은 페이지를 찾았을 때 AMP가 적용된 페이지로 연결 할 수 있는 기능이다.&lt;/li&gt;
&lt;li&gt;AMP가 적용되어있지 않은 페이지에 amp페이지를 링크하고 amp 페이지에는 non-amp페이지를 링크한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1559184738723&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 아래 코드를 NON-AMP 페이지에 추가 --&amp;gt;
&amp;lt;link rel=&quot;amphtml&quot; href=&quot;/article.amp.html&quot;&amp;gt;
&amp;lt;!-- 아래 코드를 AMP 페이지에 추가 --&amp;gt;
&amp;lt;link rel=&quot;canonical&quot; href=&quot;/article.html&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;AMP 페이지 하나만 존재하더라도 canonical 링크를 반드시 페이지에 시켜주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1559184738723&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;link rel=&quot;canonical&quot; href=&quot;/article.html&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;AMP 샘플 페이지 구동&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;상단에 게재한 샘플 페이지 코드 실행 결과&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vqbgf/btqvFXMV3a1/KHJBZwf8GjbOqBqZ7utK70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vqbgf/btqvFXMV3a1/KHJBZwf8GjbOqBqZ7utK70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vqbgf/btqvFXMV3a1/KHJBZwf8GjbOqBqZ7utK70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvqbgf%2FbtqvFXMV3a1%2FKHJBZwf8GjbOqBqZ7utK70%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul&gt;
&lt;li&gt;웹 페이지의 최적화를 통해 성능 향상을 기대 할 수 있다.&lt;/li&gt;
&lt;li&gt;구글 검색에서 상단에 노출될 확률이 높다.&lt;/li&gt;
&lt;li&gt;다양한 템플릿 제공으로 빠르게 웹 사이트를 만들어낼 수 있다. (흡사 부트 스트랩 같은 느낌)&lt;/li&gt;
&lt;li&gt;구글 애널리틱스 연동을 amp-analytics 스니펫으로 제공하고 있고 수집 정보를 유연하게 설정 할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul&gt;
&lt;li&gt;거의 모든 태그가 규격화 되어있고 일부는 AMP전용 태그를 사용해야 한다.&lt;/li&gt;
&lt;li&gt;AMP 전용 JS 라이브러리를 사용하고 커스텀하게 자바스크립트를 사용 할 수 없다.&lt;/li&gt;
&lt;li&gt;커스텀 CSS는 인라인으로만 사용해야 하며 50KB를 넘지 않아야 하고 !important같은 일부 요소는 사용이 금지 되어있다.&lt;/li&gt;
&lt;li&gt;복잡한 자바스크립트 코드 또는 역동적인 애니메이션 효과가 포함된 페이지는 AMP화 시키기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;Google AMP 개요 편&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;끝.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Article</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/52</guid>
      <comments>https://team-platform.tistory.com/52#entry52comment</comments>
      <pubDate>Thu, 30 May 2019 11:55:36 +0900</pubDate>
    </item>
    <item>
      <title>Docker : 도커스웜 클러스터 구축 편</title>
      <link>https://team-platform.tistory.com/51</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;img03.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwBCU7/btqvmsNDVPn/1JyZvOPvnNBpeOEIiuYY60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwBCU7/btqvmsNDVPn/1JyZvOPvnNBpeOEIiuYY60/img.png&quot; data-alt=&quot;Docker Swarm&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwBCU7/btqvmsNDVPn/1JyZvOPvnNBpeOEIiuYY60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwBCU7%2FbtqvmsNDVPn%2F1JyZvOPvnNBpeOEIiuYY60%2Fimg.png&quot; data-filename=&quot;img03.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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker Swarm&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;지난&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Docker : 컨테이너 오케스트레이션 개요 편&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;에서는 한 서비스가 점차 확장되면서 컨테이너 증가에 따른 관리의 필요성과 다수의 컨테이너를 효과적으로 다룰 수 있는 컨테이너 오케스트레이션 툴에 대한 개요를 설명 하였다. 이번 편에서는 도커스웜의 노드 클러스터링 구축과 스웜 로드밸런서의 기능을 확인해보는 실습을 진행하도록 하겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;도커스웜의 노드 클러스터링&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;노드 클러스터링(&lt;span style=&quot;color: #333333;&quot;&gt;Node&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;Clustering)&lt;/span&gt;&lt;/span&gt;의 노드(Node)는 도커스웜에서 물리적 또는 논리적으로 분리 된 독립적인 서버(Server)를 의미하고 클러스터링(Clustering)은 사전적 의미로 뭉치기라는 뜻을 가지고 있다. 즉 노드 클러스터링은&amp;nbsp;&lt;b&gt;서버의 군집화&lt;/b&gt;라고 표현 할 수 있다. 필자는 3개의 노드를 클러스터링 하기 위해 VirtualBox의 Clone기능을 활용하여 아래와 같이 총 3대의 노드를 생성하였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ls5LG/btqvlTD5Pyo/wffynSYhxLC4ypA4G5IJxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ls5LG/btqvlTD5Pyo/wffynSYhxLC4ypA4G5IJxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ls5LG/btqvlTD5Pyo/wffynSYhxLC4ypA4G5IJxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLs5LG%2FbtqvlTD5Pyo%2FwffynSYhxLC4ypA4G5IJxK%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;스웜 클러스터를 구성하기 위해서는 클러스터의 중심이 되는 노드가 필요한데 필자는 manager01을 매니저 노드로 설정 하였고 나머지 노드를 worker노드로 구분하였다. (색상표기가 된 manager와 worker는 각 노드의 hostname이다)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자 이제 클러스터를 생성해보자.&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;color: #f41a18;&quot;&gt;&lt;b&gt;는 잠깐!!&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJgBhK/btqviHLGNSc/3ImqFRfdHk9AdCiHUhaUAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJgBhK/btqviHLGNSc/3ImqFRfdHk9AdCiHUhaUAK/img.png&quot; data-alt=&quot;포.. 포트포워딩이 아직 안 됐....&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJgBhK/btqviHLGNSc/3ImqFRfdHk9AdCiHUhaUAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJgBhK%2FbtqviHLGNSc%2F3ImqFRfdHk9AdCiHUhaUAK%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;/&gt;&lt;/span&gt;&lt;figcaption&gt;포.. 포트포워딩이 아직 안 됐....&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;클러스터를 생성하기 전 도커스웜에서 사용하는 포트와 서비스로 사용할 포트를 모두 열어주어야 한다. VirturalBox의 포트포워딩 설정과 각 노드의 포트별로 방화벽 해제 처리를 해주자&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBpJii/btqvjMZ6JSw/VQwTBSFZcgl572ItMq34tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBpJii/btqvjMZ6JSw/VQwTBSFZcgl572ItMq34tK/img.png&quot; data-alt=&quot;VirtualBox 포트포워딩 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBpJii/btqvjMZ6JSw/VQwTBSFZcgl572ItMq34tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBpJii%2FbtqvjMZ6JSw%2FVQwTBSFZcgl572ItMq34tK%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;/&gt;&lt;/span&gt;&lt;figcaption&gt;VirtualBox 포트포워딩 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1558064371294&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 방화벽 해제
$ sudo firewall-cmd --permanent --add-port=2377/tcp
$ sudo firewall-cmd --permanent --add-port=7946/tcp
$ sudo firewall-cmd --permanent --add-port=7946/udp
$ sudo firewall-cmd --permanent --add-port=4789/udp
$ sudo firewall-cmd --permanent --add-port=80/tcp
$ sudo firewall-cmd --reload&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVRECu/btqvmtrIPMC/ttBNftwdccs3iKMKslkrS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVRECu/btqvmtrIPMC/ttBNftwdccs3iKMKslkrS1/img.png&quot; data-alt=&quot;포트포워딩이 됐...!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVRECu/btqvmtrIPMC/ttBNftwdccs3iKMKslkrS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVRECu%2FbtqvmtrIPMC%2FttBNftwdccs3iKMKslkrS1%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;/&gt;&lt;/span&gt;&lt;figcaption&gt;포트포워딩이 됐...!!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;포트포워딩 설정과 방화벽 해제가 완료 되었다면 manager01 노드에서 docker swarm init 명령으로 스웜 클러스터를 생성 해보자. init 뒤에 있는 --advertise-addr 옵션에는 해당 주소로 swarm join 할 수 있도록 IP와 포트를 지정 할 수 있다. 현재 VirtualBox에서 생성한 CentOS 7 환경에서는 Guest의 IP가 10.0.2.15로 잡혀 있으므로 VirtualBox에서 생성된 노드들 끼리 접근이 가능하려면 Host IP인 192.168.37.1로 설정 해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1558064371294&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker swarm init --advertise-addr 192.168.37.1
Swarm initialized: current node (0ba2xn5fqkv1047rc5xa77nl0) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-2aai8y68yz8ju664rktqbxlw88if54rqzx9cv49or7ywnzzpz5-b40gsplzlg9ip0o6s4ofdke29 \
    192.168.37.1:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;스웜 클러스터가 생성되면 친절하게도 위와 같이 docker swarm join 명령어 샘플이 자동으로 생성된다. 바로 아랫줄에 있는 토큰은 manager 노드로 접근하기 위한 일종의 비밀 키 이므로 실무에서 사용하는경우 외부에 노출되지 않도록 주의하길 바란다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 각각의 worker 노드에서 join 명령어를 통해 스웜 클러스터에 합류 해보자&lt;/p&gt;
&lt;pre id=&quot;code_1558064371295&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker swarm join \
&amp;gt; --token SWMTKN-1-2aai8y68yz8ju664rktqbxlw88if54rqzx9cv49or7ywnzzpz5-b40gsplzlg9ip0o6s4ofdke29 \
&amp;gt; 192.168.37.1:2377
This node joined a swarm as a worker.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;swarm join이 완료되면 'This&amp;nbsp;node&amp;nbsp;joined&amp;nbsp;a&amp;nbsp;swarm&amp;nbsp;as&amp;nbsp;a&amp;nbsp;worker.' 문구와 함께 참여완료 문구가 뜬다. 이제 다시 manager노드로 이동하여 각 worker노드들이 잘 합류했는지 확인해보자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1558064371295&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker node ls
ID                           HOSTNAME   STATUS  AVAILABILITY  MANAGER STATUS
0ba2xn5fqkv1047rc5xa77nl0 *  manager01  Ready   Active        Leader
jw2kxdt3v7tiqq9etiaxqw1lt    worker01   Ready   Active
x3kxchadl950q934kuctymzmm    worker02   Ready   Active&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3개의 노드를 하나로 묶은 클러스터링이 구축 되었다. 현재 상태를 이미지로 표현하면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y0HfO/btqviGMObRh/gXUcEHgKqcJ05SWv16vHlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y0HfO/btqviGMObRh/gXUcEHgKqcJ05SWv16vHlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y0HfO/btqviGMObRh/gXUcEHgKqcJ05SWv16vHlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY0HfO%2FbtqviGMObRh%2FgXUcEHgKqcJ05SWv16vHlK%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이제 스웜 클러스터도 생성 되었고 스웜 로드벨런싱 기능을 확인하기 위해 간단한 실습을 진행 해보도록 하겠다. 먼저 manager 노드에 docker service로 nginx를 설치 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1558064371295&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker service create \
--name my-web \
--publish published=80,target=80 \
--replicas 1 \
nginx

# docker service ls
ID            NAME    MODE        REPLICAS  IMAGE
x4njngtukyfk  my-web  replicated  1/1       nginx:latest

# docker service ps my-web
ID            NAME      IMAGE         NODE      DESIRED STATE  CURRENT STATE        ERROR  PORTS
m05xzf46mz8t  my-web.1  nginx:latest  worker01  Running        Running 2 hours ago&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: square;&quot;&gt;
&lt;li&gt;docker service create 옵션설명
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;--name: 생성할 service의 이름을 지정한다.&lt;/li&gt;
&lt;li&gt;--replicas: 생성할 컨테이너 갯수&lt;/li&gt;
&lt;li&gt;--publish: 서비스에서 사용되는 포트를 오픈한다.
&lt;ol style=&quot;list-style-type: decimal;&quot;&gt;
&lt;li&gt;published: 도커 컨테이너로 전달할 외부 포트&lt;/li&gt;
&lt;li&gt;target: 도커 컨테이너 내부 포트&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;서비스 생성 후 192.168.37.1, 2, 3번 각각 접근 해보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHu08A/btqviHSxlfV/rLRlqOqMUb16mdK5kYtgF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHu08A/btqviHSxlfV/rLRlqOqMUb16mdK5kYtgF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHu08A/btqviHSxlfV/rLRlqOqMUb16mdK5kYtgF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHu08A%2FbtqviHSxlfV%2FrLRlqOqMUb16mdK5kYtgF1%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;현재 192.168.37.1 manager 노드 하나에만 nginx 컨테이너가 활성화 되어있는데 어떻게 다른 노드로 접근해도 같은 페이지가 뜨는 것일까? 스웜 클러스터가 구축 되면&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;ingress network가 생성되는데 이것은 어떤 노드에 접근하더라도 서비스중인 컨테이너에 접근 가능하도록 R&lt;span style=&quot;color: #333333;&quot;&gt;outing mesh를 구성하고 있고&lt;/span&gt;&lt;/span&gt;&amp;nbsp;스웜 로드밸런서에 서비스가 활성화 되어있는 노드에 라운드 로빈(Round Robin)방식으로 로드 밸런싱 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;Routing mesh: 서비스에서 포트가 오픈되면 모든 노드에도 동일한 포트가 오픈되며 어떤 노드에 요청을 보내더라도 현재 서비스가 실행중인 노드의 컨테이너로 요청을 전달한다.&lt;/li&gt;
&lt;li&gt;라운드 로빈(Round Robin): 스케쥴링의 한 방식으로 리스트의 맨 위에서 아래로 가며 하나 씩 순차적으로 진행 하고 끝나면 다시 맨 위로 돌아가는 식으로 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4wq30/btqvkxIuhve/4zmhg4RbnHtjW1ifk1efQ0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4wq30/btqvkxIuhve/4zmhg4RbnHtjW1ifk1efQ0/img.gif&quot; data-alt=&quot;Round Robin 방식의 적절한 예&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4wq30/btqvkxIuhve/4zmhg4RbnHtjW1ifk1efQ0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b4wq30/btqvkxIuhve/4zmhg4RbnHtjW1ifk1efQ0/img.gif&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Round Robin 방식의 적절한 예&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 ingress network와 스웜 로드벨런서 덕분에&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;어떤 노드를 호출해도 같은 페이지를 보는것이 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHDQIE/btqvnGEbhdW/3rIF6d690LODKaeY8S1liK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHDQIE/btqvnGEbhdW/3rIF6d690LODKaeY8S1liK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHDQIE/btqvnGEbhdW/3rIF6d690LODKaeY8S1liK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHDQIE%2FbtqvnGEbhdW%2F3rIF6d690LODKaeY8S1liK%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;만약 현재의 상태로 manager 노드의 컨테이너가 죽는다면 어떤 노드로 접근한다 해도 페이지는 먹통일 것이다. 노드 클러스터링의 목적은 과도한 트래픽이 몰릴 경우를 대비한 트래픽 분산도 있지만 하나의 컨테이너가 죽더라도 다른 컨테이너로 대체 할 수 있도록 위험분산의 목적도 가지고 있다. 이번에는 전체 node에 골고루 분포 되도록 Replica의 갯수를 6개로 늘려보도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1558064371302&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker service scale my-web=6
my-web scaled to 6
# docker service ps my-web
ID            NAME          IMAGE         NODE       DESIRED STATE  CURRENT STATE               ERROR  PORTS
0mouqz0t89nz  my-web.1      nginx:latest  worker01   Running        Running about a minute ago
xx2lsv3uwcrp  my-web.2      nginx:latest  worker02   Running        Running 51 seconds ago
h6kjhoyvfpyl  my-web.3      nginx:latest  worker01   Running        Running 52 seconds ago
nszaz7kkg8wc  my-web.4      nginx:latest  manager01  Running        Running 51 seconds ago
wdgjoyib63ue  my-web.5      nginx:latest  manager01  Running        Running 51 seconds ago
zwqsm1yus5os  my-web.6      nginx:latest  worker02   Running        Running 51 seconds ago&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLMwwn/btqvlg7CVrZ/am3uNkhswNemGktfC4CI0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLMwwn/btqvlg7CVrZ/am3uNkhswNemGktfC4CI0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLMwwn/btqvlg7CVrZ/am3uNkhswNemGktfC4CI0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLMwwn%2Fbtqvlg7CVrZ%2Fam3uNkhswNemGktfC4CI0K%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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;NODE항목의 각 노드 갯수를 보면 manager와 worker들의 노드 갯수가 2개씩 배정이 되어 있는것을 확인 할 수 있고 세개의 노드 중 두개가 죽더라도 서비스에는 지장이 없게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기까지 도커스웜 클러스터 구축과 manager노드 및 worker노드의 생성,&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;ingress network의 개요와 스웜 로드밸런서를 통한 로드밸런싱 기능을 확인 해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Docker : 도커스웜 클러스터 구축 편&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;끝.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/51</guid>
      <comments>https://team-platform.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 17 May 2019 12:44:36 +0900</pubDate>
    </item>
    <item>
      <title>[스파크(Spark)] #3. 구조적 API 개요 및 기본 연산</title>
      <link>https://team-platform.tistory.com/50</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://team-platform.tistory.com/42&quot;&gt;[스파크(Spark)]&amp;nbsp;#1.&amp;nbsp;개요&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://team-platform.tistory.com/49&quot;&gt;[스파크(Spark)]&amp;nbsp;#2.&amp;nbsp;용어&amp;nbsp;및&amp;nbsp;개념&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://team-platform.tistory.com/50&quot;&gt;[스파크(Spark)]&amp;nbsp;#3.&amp;nbsp;구조적&amp;nbsp;API&amp;nbsp;개요&amp;nbsp;및&amp;nbsp;기본&amp;nbsp;연산&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 구조적 API의 개요 및 기본 연산에 대해서 알아본다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;DataFrame와 Dataset은 둘 다 Row와 Column을 가지는 불변성을 가지는 분산 테이블 형태의 컬렉션이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Dataset은 JVM 기반이므로 java와 scala를 지원하지만 Python은 지원하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;python 코드로 검증을 할 예정이므로 DataFrame 기준으로 설명한다.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;p&gt;Spark API 관련 자세한 부분은 Spark Docs를 참고하자.&lt;/p&gt;
&lt;p&gt;DataFrame을 가공 관련 부분은&amp;nbsp;&lt;a href=&quot;https://hirang0110.tistory.com/manage/newpost/pyspark.sql&quot;&gt;pyspark.sql&lt;/a&gt;&amp;nbsp;모듈을 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://spark.apache.org/docs/2.1.0/api/python/index.html&quot;&gt;Python Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SparkSession&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;Spark의 모든 기능에 대한 진입점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spark.apache.org/docs/2.1.0/api/python/pyspark.sql.html#pyspark.sql.SparkSession&quot;&gt;SparkSession&lt;/a&gt;클래스를 사용해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; livescript&quot;&gt;&lt;code&gt;from pyspark.sql import SparkSession 
spark = SparkSession \
	.builder \ .master(&quot;local&quot;) \
	.appName(&quot;Python Spark SQL basic example&quot;) \
	.config(&quot;spark.some.config.option&quot;, &quot;some-value&quot;) \
	.getOrCreate()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;- builder : 객체 생성&lt;/p&gt;
&lt;p&gt;- master : 실행 환경을 설정&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 60.6227%; height: 60px;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 41.7521%; height: 20px; text-align: left;&quot;&gt;local&lt;/td&gt;
&lt;td style=&quot;width: 58.2479%; height: 20px;&quot;&gt;로컬 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 41.7521%; height: 20px; text-align: left;&quot;&gt;local[4]&lt;/td&gt;
&lt;td style=&quot;width: 58.2479%; height: 20px;&quot;&gt;4코어로 로컬 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 41.7521%; height: 20px; text-align: left;&quot;&gt;spark : // master&amp;nbsp;:&amp;nbsp;7077&lt;/td&gt;
&lt;td style=&quot;width: 58.2479%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Spark 독립 실행형 클러스터&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;- config : 실행 옵션 설정,&amp;nbsp;SparkConf&amp;nbsp;및&amp;nbsp;SparkSession&amp;nbsp;자체&amp;nbsp;구성에&amp;nbsp;자동으로&amp;nbsp;전파&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.81em;&quot;&gt;(&lt;b&gt;SparkConf&lt;/b&gt;는&amp;nbsp;Spark의&amp;nbsp;런타임&amp;nbsp;구성&amp;nbsp;인터페이스며&amp;nbsp;이&amp;nbsp;인터페이스를&amp;nbsp;통해&amp;nbsp;사용자는&amp;nbsp;Spark&amp;nbsp;SQL과&amp;nbsp;관련된&amp;nbsp;모든&amp;nbsp;Spark&amp;nbsp;및&amp;nbsp;Hadoop&amp;nbsp;구성을&amp;nbsp;가져오고&amp;nbsp;설정할&amp;nbsp;수 있음)&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- getOrCreate : 기존 SparkSession을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;가져 오거나&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;없는 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;실더에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정된 옵션을 기반으로 새로운 SparkSession을 생성&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;DataFrame 생성&lt;/h2&gt;
&lt;p&gt;SparkSession응용 프로그램이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spark.apache.org/docs/2.1.0/sql-programming-guide.html#interoperating-with-rdds&quot;&gt;기존RDD&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, 하이브 테이블 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spark.apache.org/docs/2.1.0/sql-programming-guide.html#data-sources&quot;&gt;Spark 데이터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;소스&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&amp;nbsp;에서&amp;nbsp;&lt;/span&gt;DataFrames을 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;df = spark.read.format(&quot;json&quot;)\
    .load(&quot;D:/2015-summary.json&quot;)
print(df)
# 결과
# DataFrame[DEST_COUNTRY_NAME: string, ORIGIN_COUNTRY_NAME: string, count: bigint]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dggcYD/btqvkyUS3Zp/CkFPtn93xgWx9QZtzkZGeK/2015-summary.json?attach=1&amp;amp;knm=tfile.json&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;2015-summary.json&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;스키마&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;DataFrame의 칼럼명과 데이터 타입을 정의한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;스키마는 데이터 소스에서 얻거나(schema-on-read) 직접 정의할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# 1. 데이터 소스에서 얻는 방법
print(df.schema)
# 결과
# StructType(List(StructField(DEST_COUNTRY_NAME,StringType,true),StructField(ORIGIN_COUNTRY_NAME,StringType,true),StructField(count,LongType,true)))


# 2. 직접 정의 방법
from pyspark.sql.types import StructField, StructType, StringType, LongType
myManualSchema = StructType([
  StructField(&quot;DEST_COUNTRY_NAME&quot;, StringType(), True),
  StructField(&quot;ORIGIN_COUNTRY_NAME&quot;, StringType(), True),
  StructField(&quot;count&quot;, LongType(), True)
])
print(myManualSchema)
# 결과
# StructType(List(StructField(DEST_COUNTRY_NAME,StringType,true),StructField(ORIGIN_COUNTRY_NAME,StringType,true),StructField(count,LongType,false)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;StructType의 자세한 정보는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spark.apache.org/docs/2.1.0/api/python/pyspark.sql.html#pyspark.sql.types.StructType&quot;&gt;링크&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참조&lt;/p&gt;
&lt;h2&gt;&lt;span&gt;컬럼&lt;/span&gt;과 표현식&lt;/h2&gt;
&lt;h4&gt;컬럼&lt;/h4&gt;
&lt;p&gt;&lt;span&gt;컬럼은&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정수형이나 문자열 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단순 데이터 타입&lt;/b&gt;, 배열이나 맵 같은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;복합 데이터 타입&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;null&lt;/b&gt;&amp;nbsp;로&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구분된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;개발 언어의 기본적인 데이터 타입은 다 들어있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자세한 정보는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spark.apache.org/docs/1.6.0/api/java/org/apache/spark/sql/types/package-summary.html&quot;&gt;링크&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참조&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;컬럼&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성 및 참조는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;여러가지&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방법이 있지만 col함수나, column 함수를 사용한다. (둘 다 동일한 기능)&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;from pyspark.sql.functions import col, column
col(&quot;someColumnName&quot;)
column(&quot;someColumnName&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;표현식&lt;/h4&gt;
&lt;p&gt;여러&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;컬럼명을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입력받아 식별하고 단일 값을 만들기 위해 다양한 표현식을 각 레코드에 적용하는 함수이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;expr 함수를 사용하며, 예를 들면 expr(&quot;someCol&quot;)은 col(&quot;someCol&quot;) 구문과 동일하게 동작한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# 모두 같은 트랜스포메이션을 가진다. 
expr(&quot;someCol - 5&quot;)
col(&quot;someCol&quot;) - 5
expr(&quot;someCol&quot;) - 5

from pyspark.sql.functions import expr
expr(&quot;(((someCol + 5) * 200) - 6) = otherCol&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Record와 Row&lt;/h2&gt;
&lt;p&gt;스파크에서 DataFrame의 각 로우는 하나의 레코드며, 레코드를 Row 객체로 표현한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;DataFrame만 유일하게 스키마 정보를 가지고 있고, Row 객체는 스키마 정보를 가지고 있지 않다.&lt;/p&gt;
&lt;p&gt;그러므로 Row 객체를 직접 생성하려면 DataFrame의 스키마와 같은 순서로 값을 명시해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;from pyspark.sql import Row
myRow = Row(&quot;Hello&quot;, None, 1, False)
print(myRow[0])
print(myRow[2])
# 결과 
# Hello
# 1&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DataFrame 다루기&lt;/h2&gt;
&lt;p&gt;DataFrame의 기본적인 기능은 알았으니 실제로 DataFrame의 데이터를 다뤄보자.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;초반에 2015-summary.json 파일을 로드하여 생성된 DataFrame을 가지고 진행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4&gt;데이터 조회&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;select와 selectExpr&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;메소드&lt;/span&gt;를 사용하면 마치 테이블에 SQL 질의를 실행한 것처럼 데이터 조회&lt;/b&gt;를 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;select 메소드를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# df DataFrame 전체 Row 개수 조회
print(df.count)
# 256

# DataFrame 5개의 로우 조회
df.show(5)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |    United States|            Romania|   15|
# |    United States|            Croatia|    1|
# |    United States|            Ireland|  344|
# |            Egypt|      United States|   15|
# |    United States|              India|   62|
# +-----------------+-------------------+-----+

# DataFrame의 DEST_COUNTRY_NAME 컬럼으로 2개 로우 조회
df.select(&quot;DEST_COUNTRY_NAME&quot;).show(2)
# 결과
# +-----------------+
# |DEST_COUNTRY_NAME|
# +-----------------+
# |    United States|
# |    United States|
# +-----------------+

# DataFrame의 DEST_COUNTRY_NAME, ORIGIN_COUNTRY_NAME 컬럼으로 2개의 로우 조회
df.select(&quot;DEST_COUNTRY_NAME&quot;, &quot;ORIGIN_COUNTRY_NAME&quot;).show(2)
# 결과
# +-----------------+-------------------+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|
# +-----------------+-------------------+
# |    United States|            Romania|
# |    United States|            Croatia|
# +-----------------+-------------------+

# DEST_COUNTRY_NAME 컬럼을 3가지 방법으로 조회
from pyspark.sql.functions import expr, col, column
df.select(
    expr(&quot;DEST_COUNTRY_NAME&quot;),
    col(&quot;DEST_COUNTRY_NAME&quot;),
    column(&quot;DEST_COUNTRY_NAME&quot;))\
  .show(2)
# 결과
# +-----------------+-----------------+-----------------+
# |DEST_COUNTRY_NAME|DEST_COUNTRY_NAME|DEST_COUNTRY_NAME|
# +-----------------+-----------------+-----------------+
# |    United States|    United States|    United States|
# |    United States|    United States|    United States|
# +-----------------+-----------------+-----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select 메소드 안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;컬럼&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;지정을 expr 함수로 같이 사용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.81em;&quot;&gt;함수와&amp;nbsp;&lt;span&gt;메소드&lt;/span&gt;의&amp;nbsp;차이는&amp;nbsp;객체(Object)에&amp;nbsp;속해있으면 메소드, 속해있지 않으면 함수로 보면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 0.81em;&quot;&gt;예)&amp;nbsp;print(&amp;nbsp;),&amp;nbsp;type(&amp;nbsp;), str(&amp;nbsp;), bool(&amp;nbsp;)&amp;nbsp;등과&amp;nbsp;같이&amp;nbsp;자료형을&amp;nbsp;조회하거나 변경 시&amp;nbsp; 사용하는&amp;nbsp;것들은 모두&amp;nbsp;함수&lt;/p&gt;
&lt;p style=&quot;font-size: 0.81em;&quot;&gt;예)&amp;nbsp;리스트를&amp;nbsp;기준으로&amp;nbsp;index(&amp;nbsp;),&amp;nbsp;count(&amp;nbsp;),&amp;nbsp;append(&amp;nbsp;),&amp;nbsp;remove(&amp;nbsp;),&amp;nbsp;reverse(&amp;nbsp;)는&amp;nbsp;객체와&amp;nbsp;관련이&amp;nbsp;있으므로&amp;nbsp;메소드&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# DEST_COUNTRY_NAME 컬럼을 destination명으로 변경하여 조회
from pyspark.sql.functions import expr, col, column
df.select(expr(&quot;DEST_COUNTRY_NAME AS destination&quot;)).show(2)
# 결과
# +-------------+
# |  destination|
# +-------------+
# |United States|
# |United States|
+-------------+

# DEST_COUNTRY_NAME 컬럼을 destination명으로 변경하고 또 DEST_COUNTRY_NAME 이름으로 변경
df.select(expr(&quot;DEST_COUNTRY_NAME as destination&quot;).alias(&quot;DEST_COUNTRY_NAME&quot;))\
  .show(2)
# 결과
# +-----------------+
# |DEST_COUNTRY_NAME|
# +-----------------+
# |    United States|
# |    United States|
# +-----------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select 메서드에 expr 함수를 사용하는 패턴이 많아져서 스파크는 이런 작업을 효율적으로 할수 있는 &lt;b&gt;selectExpr 메서드&lt;/b&gt;를 제공한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;df.selectExpr(&quot;DEST_COUNTRY_NAME as newColumnName&quot;, &quot;DEST_COUNTRY_NAME&quot;).show(2)
# 결과
# +-------------+-----------------+
# |newColumnName|DEST_COUNTRY_NAME|
# +-------------+-----------------+
# |United States|    United States|
# |United States|    United States|
# +-------------+-----------------+

df.selectExpr(
  &quot;*&quot;, # all original columns
  &quot;(DEST_COUNTRY_NAME = ORIGIN_COUNTRY_NAME) as withinCountry&quot;)\
  .show(2)
# 결과
# +-----------------+-------------------+-----+-------------+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|withinCountry|
# +-----------------+-------------------+-----+-------------+
# |    United States|            Romania|   15|        false|
# |    United States|            Croatia|    1|        false|
# +-----------------+-------------------+-----+-------------+

df.selectExpr(&quot;avg(count)&quot;, &quot;count(distinct(DEST_COUNTRY_NAME))&quot;).show(2)
# 결과
# [Stage 8:====================================================&amp;gt;  (191 + 1) / 200]+-----------+---------------------------------+
# | avg(count)|count(DISTINCT DEST_COUNTRY_NAME)|
# +-----------+---------------------------------+
# |1770.765625|                              132|
# +-----------+---------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;distinct 메서드를 이용하여 중복을 제거하고&lt;span style=&quot;background-color: #ffffff;&quot;&gt; 고유한&lt;/span&gt; 로우를 얻을 수도 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt; # ORIGIN_COUNTRY_NAME 컬럼의 로우에 대한 중복을 제거 하고 데이터 및 개수 조회
df5 = df.select(&quot;ORIGIN_COUNTRY_NAME&quot;).distinct()
df5.show(5)
print(df.count())
# 결과
# +-------------------+
# |ORIGIN_COUNTRY_NAME|
# +-------------------+
# |           Paraguay|
# |             Russia|
# |           Anguilla|
# |            Senegal|
# |             Sweden|
# +-------------------+
# only showing top 5 rows
# 256
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sort 와 orderBy 메서드를 사용하여 정렬할 수 있다 .&lt;/p&gt;
&lt;p&gt;기본 동작은 asc(오름차순)이다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# sort 메소드를 사용하여 count 컬럼 정렬 5 로우 조회
df.sort(&quot;count&quot;).show(5)
# 결과
# +--------------------+-------------------+-----+
# |   DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +--------------------+-------------------+-----+
# |               Malta|      United States|    1|
# |Saint Vincent and...|      United States|    1|
# |       United States|            Croatia|    1|
# |       United States|          Gibraltar|    1|
# |       United States|          Singapore|    1|
# +--------------------+-------------------+-----+

# orderBy 메소드를 사용하여 count, DEST_COUNTRY_NAME 정렬 5 로우 조회
df.orderBy(&quot;count&quot;, &quot;DEST_COUNTRY_NAME&quot;).show(5)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |     Burkina Faso|      United States|    1|
# |    Cote d'Ivoire|      United States|    1|
# |           Cyprus|      United States|    1|
# |         Djibouti|      United States|    1|
# |        Indonesia|      United States|    1|
# +-----------------+-------------------+-----+
 
df.orderBy(col(&quot;count&quot;), col(&quot;DEST_COUNTRY_NAME&quot;)).show(5)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |     Burkina Faso|      United States|    1|
# |    Cote d'Ivoire|      United States|    1|
# |           Cyprus|      United States|    1|
# |         Djibouti|      United States|    1|
# |        Indonesia|      United States|    1|
# +-----------------+-------------------+-----+

from pyspark.sql.functions import desc, asc

# orderBy 메소드를 사용하여 count 내림차순 2 로우 조회
df.orderBy(expr(&quot;count desc&quot;)).show(2)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |          Moldova|      United States|    1|
# |    United States|            Croatia|    1|
# +-----------------+-------------------+-----+

# orderBy 메소드를 사용하여 count 내림차순, DEST_COUNTRY_NAME 오름차순 2 로우 조회
df.orderBy(col(&quot;count&quot;).desc(), col(&quot;DEST_COUNTRY_NAME&quot;).asc()).show(2)
# 결과
# +-----------------+-------------------+------+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME| count|
# +-----------------+-------------------+------+
# |    United States|      United States|370002|
# |    United States|             Canada|  8483|
# +-----------------+-------------------+------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;컬럼 추가와 컬럼명 변경&lt;/h4&gt;
&lt;p&gt;컬럼 추가는 DataFrame의 &lt;b&gt;withColumn&lt;/b&gt; 메서드를 이용한다.&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# numberOne이라는 컬럼을 추가
from pyspark.sql.functions import lit
df.withColumn(&quot;numberOne&quot;, lit(1)).show(2)
# 결과
# +-----------------+-------------------+-----+---------+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|numberOne|
# +-----------------+-------------------+-----+---------+
# |    United States|            Romania|   15|        1|
# |    United States|            Croatia|    1|        1|
# +-----------------+-------------------+-----+---------+

# expr 조건을 통하여 boolean 타입으로 withinCountry 컬럼을 추가
df.withColumn(&quot;withinCountry&quot;, expr(&quot;ORIGIN_COUNTRY_NAME == DEST_COUNTRY_NAME&quot;))\
  .show(2)
# 결과 
# +-----------------+-------------------+-----+-------------+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|withinCountry|
# +-----------------+-------------------+-----+-------------+
# |    United States|            Romania|   15|        false|
# |    United States|            Croatia|    1|        false|
# +-----------------+-------------------+-----+-------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컬럼명 변경은 &lt;b&gt;withColumnRenamed&lt;/b&gt; 메서드를 이용한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# DEST_COUNTRY_NAME 컬럼을 dest로 변경
df.show(2)
df1 = df.withColumnRenamed(&quot;DEST_COUNTRY_NAME&quot;, &quot;dest&quot;)
df1.show(2)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |    United States|            Romania|   15|
# |    United States|            Croatia|    1|
# +-----------------+-------------------+-----+
# +-------------+-------------------+-----+
# |         dest|ORIGIN_COUNTRY_NAME|count|
# +-------------+-------------------+-----+
# |United States|            Romania|   15|
# |United States|            Croatia|    1|
# +-------------+-------------------+-----+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;대소문자 구분&lt;/h4&gt;
&lt;p&gt;스파크는 기본적으로 대소문자를 구분하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대소문자를 구분하게 만드려면 아래의 3가지 방법 중 하나의 방법에 대한 코드을 추가한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# 1. SparkSession을 생성할 때 config 옵션에 넣는다. 
spark = SparkSession.builder \
    .master(&quot;local&quot;) \
    .appName(&quot;testapp&quot;) \
    .config(&quot;spark.some.config.option&quot;, &quot;some-value&quot;) \
    .config(&quot;spark.sql.caseSensitive&quot;, &quot;true&quot;) \
    .getOrCreate()

# 2. spark.conf.set 함수를 이용
spark.conf.set(&quot;spark.sql.caseSensitive&quot;, &quot;true&quot;)

# 3. spark.sql 함수 이용
spark.sql(&quot;SET spark.sql.caseSensitive=true&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;컬럼 제거&lt;/h4&gt;
&lt;p&gt;select 메서드로 컬럼을 제거할 수 있지만 drop 메서드를 사용할 수도 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# ORIGIN_COUNTRY_NAME 컬럼 제거
df2 = df.drop(&quot;ORIGIN_COUNTRY_NAME&quot;)
df2.show(2)
# 결과 
# +-----------------+-----+
# |DEST_COUNTRY_NAME|count|
# +-----------------+-----+
# |    United States|   15|
# |    United States|    1|
# +-----------------+-----+

# ORIGIN_COUNTRY_NAME, DEST_COUNTRY_NAME 2 개의 컬럼 제거
df3 = df.drop(&quot;ORIGIN_COUNTRY_NAME&quot;, &quot;, DEST_COUNTRY_NAME&quot;)
df3.show(2)
# 결과
# +-----+
# |count|
# +-----+
# |   15|
# |    1|
# +-----+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;컬럼 타입 변경&lt;/h4&gt;
&lt;p&gt;특정 컬럼의 데이터 타입을 다른 데이터 타입으로 형변환할 경우가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;cast 메서드를 이용하여 데이터 타입을 변환할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt;# LongType의 count 컬럼을 StringType의 count2 컬럼으로 변경
print(df.schema) # 기존 스키마 조회
df4 = df.withColumn(&quot;count2&quot;, col(&quot;count&quot;).cast(&quot;string&quot;))
df4.show(2)
print(df4.schema) # 변경된 스키마 조회
# 결과
# StructType(List(StructField(DEST_COUNTRY_NAME,StringType,true),StructField(ORIGIN_COUNTRY_NAME,StringType,true),StructField(count,LongType,true)))
# +-----------------+-------------------+-----+------+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|count2|
# +-----------------+-------------------+-----+------+
# |    United States|            Romania|   15|    15|
# |    United States|            Croatia|    1|     1|
# +-----------------+-------------------+-----+------+
# only showing top 2 rows
# StructType(List(StructField(DEST_COUNTRY_NAME,StringType,true),StructField(ORIGIN_COUNTRY_NAME,StringType,true),StructField(count,LongType,true),StructField(count2,StringType,true)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;로우 필터링&lt;/h4&gt;
&lt;p&gt;로우를 필터링 하려면 참과 거짓을 판별하는 표현식을 만들어야 한다. 그래서 표현식의 결과가 false인 로우를 걸러내면 된다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;where&lt;/b&gt; 메서드와 &lt;b&gt;filter&lt;/b&gt; 메서드를 이용한다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;brush:python; gauss&quot;&gt;&lt;code&gt; # count가 2 이상인 로우 2개 조회
df.filter(col(&quot;count&quot;) &amp;lt; 2 ).show(2)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |    United States|            Croatia|    1|
# |    United States|          Singapore|    1|
# +-----------------+-------------------+-----+

df.where(&quot;count &amp;lt; 2&quot;).show(2)
# 결과
# +-----------------+-------------------+-----+
# |DEST_COUNTRY_NAME|ORIGIN_COUNTRY_NAME|count|
# +-----------------+-------------------+-----+
# |    United States|            Croatia|    1|
# |    United States|          Singapore|    1|
# +-----------------+-------------------+-----+&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;마치며&lt;/h2&gt;
&lt;p&gt;구조적 API Dataframd의 기본적인 연산을 확인해 보았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다름에는 다양한 데이터 타입에 대해서 알아본다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Spark</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/50</guid>
      <comments>https://team-platform.tistory.com/50#entry50comment</comments>
      <pubDate>Fri, 17 May 2019 11:27:36 +0900</pubDate>
    </item>
    <item>
      <title>[스파크(Spark)] #2. 용어 및 개념</title>
      <link>https://team-platform.tistory.com/49</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;176&quot; height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxyVt6/btquW400nqe/KsdKcrdnL3XGqFhFZ4W0h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxyVt6/btquW400nqe/KsdKcrdnL3XGqFhFZ4W0h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxyVt6/btquW400nqe/KsdKcrdnL3XGqFhFZ4W0h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxyVt6%2FbtquW400nqe%2FKsdKcrdnL3XGqFhFZ4W0h1%2Fimg.png&quot; width=&quot;176&quot; height=&quot;94&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://team-platform.tistory.com/42&quot;&gt;[스파크(Spark)]&amp;nbsp;#1.&amp;nbsp;개요&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://team-platform.tistory.com/49&quot;&gt;[스파크(Spark)]&amp;nbsp;#2.&amp;nbsp;용어&amp;nbsp;및&amp;nbsp;개념&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://team-platform.tistory.com/50&quot;&gt;[스파크(Spark)]&amp;nbsp;#3.&amp;nbsp;구조적&amp;nbsp;API&amp;nbsp;개요&amp;nbsp;및&amp;nbsp;기본&amp;nbsp;연산&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;스파크가 무엇인지에 대한 개요에 대해서 알아보았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 핵심 용어 및 개념에 대해서 알아본다.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;스파크 애플리케이션 아키텍처&lt;/h3&gt;
&lt;p&gt;사용자는 클러스터 매니저에게 스파크 애플리케이션을 제출한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;클러스터 매니저는 제출받은 애플리케이션 실행에 필요한 자원을 할당하고, 스파크 애플리케이션은 할당받은 자원으로 작업을 처리한다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; width=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boq9FY/btquXK1RMgC/r4S1ZKLP6tQ1dKK9Kio50K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boq9FY/btquXK1RMgC/r4S1ZKLP6tQ1dKK9Kio50K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boq9FY/btquXK1RMgC/r4S1ZKLP6tQ1dKK9Kio50K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fboq9FY%2FbtquXK1RMgC%2Fr4S1ZKLP6tQ1dKK9Kio50K%2Fimg.png&quot; width=&quot;660&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;스파크 애플리케이션은 드라이버 프로세스와 다수의 익스큐터 프로세스로 구성된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;드라이버 프로세스는 클러스터 노드 중 하나에에서만 실행한다. 즉&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;main() 함수를 실행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;익스큐더는 다수의 도드에서 실행하며, 드라이버가 할당한 작업을 수행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용자는 각 노드에 할당할 익스큐터 수를 지정할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;클러스터 매니저는&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;스파크가 연산에 사용할 4개의 클러스터 종류를 지원한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.5116%;&quot;&gt;드라이버(driver)&lt;/td&gt;
&lt;td style=&quot;width: 63.4884%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;하나의 스파크 애플리케이션 처리&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프로그램이나 입력에 대한 응답&lt;/li&gt;
&lt;li&gt;익스큐터 작업과 관련된 분석, 배포 및 스케쥴링 역활 수행&lt;/li&gt;
&lt;li&gt;SparkSession라고 하며 스파크 애플리케이션의 엔트리 역활을 맡은 object임&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.5116%;&quot;&gt;익스큐터(executor)&lt;/td&gt;
&lt;td style=&quot;width: 63.4884%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;드라이버가 할당한 코드를 실행&lt;/li&gt;
&lt;li&gt;실행한 진행상황을 드라이버에 보고&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;b&gt;스파크는 사용 가능한 자원을 파악하기 위해 클러스터 매니저를 사용한다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;드라이버 프로세스는 주어진 작업을 완료하기 위해 익스큐터에게 명령을 내린다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h3&gt;트랜스포메이션과 액션&lt;/h3&gt;
&lt;p&gt;스파크 핵심 데이터 구조는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #f41a18;&quot;&gt;&lt;b&gt;불변성&lt;/b&gt;&lt;/span&gt;이다. 즉 데이터를 한번 생성하면 변경할 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데이터를 변경하려면 스파크에게 알려줘야 한다. 이 때 사용하는 명령이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0051a1;&quot;&gt;&lt;b&gt;트랜스포메이션&lt;/b&gt;&lt;/span&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용자는 트랜스포메이션을 사용해 논리적 실행 계획을 만든다.&amp;nbsp; 하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0051a1;&quot;&gt;&lt;b&gt;액션&lt;/b&gt;&lt;/span&gt;을 호출하지 않으면 스파크는 실제 트랜스포메이션을 실행하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용자는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #0051a1;&quot;&gt;&lt;b&gt;트랜스포메이션&lt;/b&gt;&lt;/span&gt;을 사용해 논리적 실행 계획을 세우고&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #0051a1;&quot;&gt;&lt;b&gt;액션&lt;/b&gt;&lt;/span&gt;을 통하여 실제 연산을 수행한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;아래 python 예제를 참조하자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;#트랜스포메이션&lt;/span&gt; divisBy2 = myRange.&lt;span&gt;where&lt;/span&gt;(&lt;span&gt;&quot;number % 2 = 0&quot;&lt;/span&gt;) &lt;span&gt;#액션&lt;/span&gt; divisBy2.&lt;span&gt;count&lt;/span&gt;()&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;트랜트포메이션은 두가지 유형의 의존성 존재한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;좁은 의존성&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;하나의 입력 파티션이 하나의 출력 파티션에만 영향을 미침&lt;/li&gt;
&lt;li&gt;파이프라이닝&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;넓은 의존성&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;하나의 입력 파티션이 여러 출력 파티션에 영향을 미침&lt;/li&gt;
&lt;li&gt;셔플&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;파티션 관련 내용은 DataFrame을 먼저 참조하길 바란다.&lt;/p&gt;
&lt;h3&gt;스파크 기본 요소&lt;/h3&gt;
&lt;p&gt;스파크는 저수준의 API, 구조적 API, 그리고 추가로 제공하는 일련의 표준 라이브러리로 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;기본요소.png&quot; width=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KdZeJ/btquW39r2Of/od3Kdj2e1MmFM6kjXBSSCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KdZeJ/btquW39r2Of/od3Kdj2e1MmFM6kjXBSSCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KdZeJ/btquW39r2Of/od3Kdj2e1MmFM6kjXBSSCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKdZeJ%2FbtquW39r2Of%2Fod3Kdj2e1MmFM6kjXBSSCk%2Fimg.png&quot; data-filename=&quot;기본요소.png&quot; width=&quot;500&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;저수준의 API와 구조적 API 차이는 데이터 스키마 여부와 추상화에 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;API 특징&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 102px;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 62px;&quot;&gt;
&lt;td style=&quot;width: 27.0929%; height: 62px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;RDD - Spark 1.0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.9071%; height: 62px;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Resilient&amp;nbsp;Distributed&amp;nbsp;Dataset : 탄력적이면서 분산된 데이터 셋&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;스키마가 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한번 정의하면 변경 불가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;장애 발생 시 복구 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;현재는 RDD 비중이 높으나 Dataset이 비중이 늘어나고 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 27.0929%; height: 20px;&quot;&gt;DataFrame - Spark 1.3&lt;/td&gt;
&lt;td style=&quot;width: 72.9071%; height: 20px;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;스키마를 가진 RDD&lt;/li&gt;
&lt;li&gt;RDB 테이블과 비슷하게 명명된 열로 구성됨&lt;/li&gt;
&lt;li&gt;RDD 처럼 한번 정의하면 변경 불가능&lt;/li&gt;
&lt;li&gt;질의나 API를 통해 데이터를 쉽게 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 27.0929%; height: 20px;&quot;&gt;DataSet - Spark 1.6, 2.0&lt;/td&gt;
&lt;td style=&quot;width: 72.9071%; height: 20px;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;Spark 2.0 부터는 DataFrame과 통합되어 강력한 형식의 API와 형식지 지정되지 않은 API를 사용&lt;/li&gt;
&lt;li&gt;lambda 사용 가능&lt;/li&gt;
&lt;li&gt;정적 타이핑 및 런타임 유형이 안전&amp;nbsp;&lt;/li&gt;
&lt;li&gt;저수준의 API를 굳이 사용할 필요가 없으면 DataSet을 권장함&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;API 차이점&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; width=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5MypE/btquZdbyxQr/59gyaFNEfcy414o0f3ouk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5MypE/btquZdbyxQr/59gyaFNEfcy414o0f3ouk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5MypE/btquZdbyxQr/59gyaFNEfcy414o0f3ouk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5MypE%2FbtquZdbyxQr%2F59gyaFNEfcy414o0f3ouk0%2Fimg.png&quot; width=&quot;660&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;마치며&lt;/h3&gt;
&lt;p&gt;스파크의 기본 용어 및 개념에 대해서 알아보았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음에는 구조적 API와 기본연산에 대해서 알아본다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고&lt;/p&gt;
&lt;ul style=&quot;list-style-type: square;&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.zenika.com/a-comparison-between-rdd-dataframe-and-dataset-in-spark-from-a-developers-point-of-view-a539b5acf734&quot;&gt;https://medium.zenika.com/a-comparison-between-rdd-dataframe-and-dataset-in-spark-from-a-developers-point-of-view-a539b5acf734&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://databricks.com/blog/2016/07/14/a-tale-of-three-apache-spark-apis-rdds-dataframes-and-datasets.html&quot;&gt;https://databricks.com/blog/2016/07/14/a-tale-of-three-apache-spark-apis-rdds-dataframes-and-datasets.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/31508083/difference-between-dataframe-dataset-and-rdd-in-spark&quot;&gt;https://stackoverflow.com/questions/31508083/difference-between-dataframe-dataset-and-rdd-in-spark&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1556851535662&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/31508083/difference-between-dataframe-dataset-and-rdd-in-spark&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;position: relative; border-right: 1px solid #d9d9d9; width: 200px; height: 200px; background-size: cover; background-position: center center; background-image: url('https://scrap.kakaocdn.net/dn/bgWy1F/hyA0n5hc9X/RD7g9QVy9zj8dK05EX6zo0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/8nBEP/hyA0iQqLdP/b4IHjCB24xhkt88I5epUC0/img.png?width=1598&amp;amp;height=928&amp;amp;face=0_0_1598_928,https://scrap.kakaocdn.net/dn/dZlwrv/hyAYTLvOM4/1enwp6GOx9VEMfkWG0tZfk/img.png?width=1804&amp;amp;height=756&amp;amp;face=0_0_1804_756');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot; style=&quot;position: relative; flex-grow: 1; height: 130px; padding-left: 40px;&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;line-height: 1.6; color: #000000; font-size: 22px; padding-bottom: 10px; max-width: 467px; text-overflow: ellipsis; white-space: nowrap; margin: 0px; overflow: hidden; font-family: 'Noto Sans', 'Noto Sans KR';&quot;&gt;Difference between DataFrame, Dataset, and RDD in Spark&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;line-height: normal; margin: 0px; max-width: 467px; text-overflow: ellipsis; overflow: hidden; font-family: 'Noto Sans DemiLight', sans-serif; font-size: 14px; font-weight: 300; font-style: normal; font-stretch: normal; letter-spacing: normal; color: #909090; max-height: 42px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; display: -webkit-box;&quot;&gt;I'm just wondering what is the difference between an RDD and DataFrame (Spark 2.0.0 DataFrame is a mere type alias for Dataset[Row]) in Apache Spark? Can you convert one to the other?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;line-height: 1.6; margin: 1px auto 0px; position: absolute; bottom: -8px; font-family: AvenirNext; font-size: 14px; color: #909090;&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1556851535662&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a href=&quot;https://databricks.com/blog/2016/07/14/a-tale-of-three-apache-spark-apis-rdds-dataframes-and-datasets.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cvNyjV/hyA0jIzknz/VWzklBgUnkKVWKNeiJKZoK/img.png?width=1024&amp;amp;height=573&amp;amp;face=0_0_1024_573,https://scrap.kakaocdn.net/dn/fBQTn/hyAYSFPNmj/EdtUmCaNku2hLpTovMJFB0/img.png?width=1999&amp;amp;height=427&amp;amp;face=0_0_1999_427,https://scrap.kakaocdn.net/dn/N3n5W/hyAYSMACPM/2kSx2OzsoqUuW7pgwI6Kmk/img.png?width=1024&amp;amp;height=573&amp;amp;face=0_0_1024_573');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot; style=&quot;position: relative; flex-grow: 1; height: 130px; padding-left: 40px;&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;line-height: 1.6; color: #000000; font-size: 22px; padding-bottom: 10px; max-width: 467px; text-overflow: ellipsis; white-space: nowrap; margin: 0px; overflow: hidden; font-family: 'Noto Sans', 'Noto Sans KR';&quot;&gt;A Tale of Three Apache Spark APIs: RDDs vs DataFrames and Datasets&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;line-height: normal; margin: 0px; max-width: 467px; text-overflow: ellipsis; overflow: hidden; font-family: 'Noto Sans DemiLight', sans-serif; font-size: 14px; font-weight: 300; font-style: normal; font-stretch: normal; letter-spacing: normal; color: #909090; max-height: 42px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; display: -webkit-box;&quot;&gt;In summation, the choice of when to use RDD or DataFrame and/or Dataset seems obvious. While the former offers you low-level functionality and control, the latter allows custom view and structure, offers high-level and domain specific operations, saves spa&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;line-height: 1.6; margin: 1px auto 0px; position: absolute; bottom: -8px; font-family: AvenirNext; font-size: 14px; color: #909090;&quot;&gt;databricks.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1556851535662&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a href=&quot;https://medium.zenika.com/a-comparison-between-rdd-dataframe-and-dataset-in-spark-from-a-developers-point-of-view-a539b5acf734&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cVLq4M/hyAYURci6i/QAIvrwJsoTHYokHFSU0kgK/img.png?width=376&amp;amp;height=200&amp;amp;face=0_0_376_200,https://scrap.kakaocdn.net/dn/jk3YE/hyA0medXRD/AExoHFIC7r8xvcZW8faYp0/img.png?width=612&amp;amp;height=512&amp;amp;face=0_0_612_512,https://scrap.kakaocdn.net/dn/bsWnj0/hyA0iXchBc/bGAzv1Zf6cKvUwJo95WchK/img.png?width=783&amp;amp;height=319&amp;amp;face=0_0_783_319');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot; style=&quot;position: relative; flex-grow: 1; height: 130px; padding-left: 40px;&quot;&gt;
&lt;p class=&quot;og-title&quot; style=&quot;line-height: 1.6; color: #000000; font-size: 22px; padding-bottom: 10px; max-width: 467px; text-overflow: ellipsis; white-space: nowrap; margin: 0px; overflow: hidden; font-family: 'Noto Sans', 'Noto Sans KR';&quot;&gt;A comparison between RDD, DataFrame and Dataset in Spark from a developer&amp;rsquo;s point of view&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; style=&quot;line-height: normal; margin: 0px; max-width: 467px; text-overflow: ellipsis; overflow: hidden; font-family: 'Noto Sans DemiLight', sans-serif; font-size: 14px; font-weight: 300; font-style: normal; font-stretch: normal; letter-spacing: normal; color: #909090; max-height: 42px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; display: -webkit-box;&quot;&gt;APIs in Spark are great and contribute to the awesomeness of Spark. This so helpful framework is used to process big data.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; style=&quot;line-height: 1.6; margin: 1px auto 0px; position: absolute; bottom: -8px; font-family: AvenirNext; font-size: 14px; color: #909090;&quot;&gt;medium.zenika.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spark</category>
      <category>Spark</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/49</guid>
      <comments>https://team-platform.tistory.com/49#entry49comment</comments>
      <pubDate>Fri, 3 May 2019 11:46:23 +0900</pubDate>
    </item>
    <item>
      <title>Docker : 컨테이너 오케스트레이션 개요 편</title>
      <link>https://team-platform.tistory.com/48</link>
      <description>&lt;h3&gt;&lt;b&gt;컨테이너 오케스트레이션 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;도커 컨테이너의 갯수가 꾸준히 늘어나면 필요한 자원도 지속적으로 늘어나기 마련이다 때문에 서버 또한 여러대로 늘어날 수 있는데 한대 두대의 수준이 아니라 몇 십 몇 백대의 서버로 늘어났다고 가정 해보자 이 많은 서버들을 일일이 접근하여 명령어 날려주고 컨테이너 올리고 &quot;어? 이건 또 왜 내려갔어?&quot; 하다가 시간은 시간대로 흘러버리고 정신을 차려보면 라꾸라꾸 침대가 본인 의자옆에 있는것을 발견 할 수 있을것이다. 결론적으로 이 많은 서버들과 컨테이너를 소수의 인원으로 관리하기에는 상당히 어렵고 이 문제를 효율적으로 관리하기 위해 컨테이너 오케스트레이션 툴들이 나오게 되었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;컨테이너 오케스트레이션 툴 소개&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;img01.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFns6f/btquDM0P3RF/021SIEED8x1PgVe6KxLYHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFns6f/btquDM0P3RF/021SIEED8x1PgVe6KxLYHk/img.png&quot; data-alt=&quot;Docker Swarm / Kubernetes / Apache Mesos&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFns6f/btquDM0P3RF/021SIEED8x1PgVe6KxLYHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFns6f%2FbtquDM0P3RF%2F021SIEED8x1PgVe6KxLYHk%2Fimg.png&quot; data-filename=&quot;img01.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;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker Swarm / Kubernetes / Apache Mesos&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;컨테이너 오케스트레이션 툴의 기능에는 단순 컨테이너의 배포 뿐만이 아닌 하나의 서비스를 관리하고 유지보수 하기 위한 많은 기능들을 포함하고 있고 툴마다 기능에 대한 편차는 있으나 주요 기능은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;오케스트레이션 툴의 기능&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;노드 클러스터링&lt;/li&gt;
&lt;li&gt;컨테이너 로드 밸런싱&lt;/li&gt;
&lt;li&gt;컨테이너의 배포와 복제 자동화&lt;/li&gt;
&lt;li&gt;컨테이너 장애 복구기능&lt;/li&gt;
&lt;li&gt;컨테이너 자동 확장 및 축소&lt;/li&gt;
&lt;li&gt;컨테이너 스케쥴링&lt;/li&gt;
&lt;li&gt;로깅 및 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;컨테이너 오케스트레이션 툴의 종류는 여러가지가 있지만 그 중 가장 인지도가 높고 자주 소개되는 Docker Swarm, Kubernetes, Apache Mesos에 대해서 각 툴의 장점과 단점을 간략히 설명 하도록 하겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;Docker Swarm&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;도커 스웜은&amp;nbsp;도커 컨테이너&amp;nbsp;플랫폼에&amp;nbsp;통합&amp;nbsp;된 컨테이너&amp;nbsp;오케스트레이션 툴이다. 최초에 도커 스웜은 도커와 별개로 개발 되었으나 도커 1.12 버전부터 도커 스웜 모드라는 이름으로 합쳐졌다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: 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&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;타 오케스트레이션 툴에 비해 기능이 단순하여 세부적인 설정이 어려움&lt;/li&gt;
&lt;li&gt;초대형 노드 클러스터링에는 무리가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;Kubernetes&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;쿠버네티스는 구글에서 개발한 2014년 오픈소스화 된 프로젝트이다. 무려 15년에 걸친 구글의 대규모 운영 워크로드 운영 경험과 노하우가 축적된 프로젝트로 컨테이너 중심의 관리환경을 제공한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;현재 가장 인지도가 높고 기능이 많은 오케스트레이션 툴&lt;/li&gt;
&lt;li&gt;내장된 기능이 많아 타사 애드온이 불필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;쿠버네티스의 구성과 개념에 대한 이해가 필요하다.&lt;/li&gt;
&lt;li&gt;학습해야할 부분이 많고 소규모 프로젝트에서 구축하기 쉽지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;Apache Mesos&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아파치 메소스는&amp;nbsp;&lt;span&gt;Twitter, Apple, Uber, Netflix 등 대형 서비스를 운영하고 있는 기업에서 다수 채택 되었으며 마이크로서비스와 빅데이터, 실시간 분석, 엘라스틱 스케일링 기능 등을 제공하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;대형 서비스를 운영중인 회사에서 많이 채택 되었고 안정성이 검증되었다.&lt;/li&gt;
&lt;li&gt;수만대의 물리적 시스템으로 확장 가능하게 설계 되어있다.&lt;/li&gt;
&lt;li&gt;Zookeeper, Hadoop, Spark와 같은 응용프로그램을 연동하여 노드 클러스터링과 자원 최적화 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;너무 다양한 응용프로그램의 연동으로 인하여 복잡해질 수 있다.&lt;/li&gt;
&lt;li&gt;설치 및 관리가 어렵고 컨테이너를 활용하기 위해 Marathon 프레임워크를 추가로 설치해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.25em;&quot;&gt;&lt;b&gt;결론 : 이럴 때 적합!&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Docker Swarm&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;중소형 프로젝트일 경우&lt;/li&gt;
&lt;li&gt;관리 할 노드가 적고 많은 기능이 필요하지 않을 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Kubernetes&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;대형 프로젝트일 경우&lt;/li&gt;
&lt;li&gt;세밀하고 다양한 설정 기능이 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Apache Mesos&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;대형 프로젝트일 경우&lt;/li&gt;
&lt;li&gt;검증된 오케스트레이션 툴을 찾고있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Docker : 컨테이너 오케스트레이션 개요 편&lt;/b&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;b&gt;끝.&lt;/b&gt;&lt;/h3&gt;</description>
      <category>Docker</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/48</guid>
      <comments>https://team-platform.tistory.com/48#entry48comment</comments>
      <pubDate>Fri, 19 Apr 2019 14:52:21 +0900</pubDate>
    </item>
    <item>
      <title>JAVA를 이용한 구글 캘린더 API 연동</title>
      <link>https://team-platform.tistory.com/47</link>
      <description>&lt;p&gt;외부에서 제작한 회의실 예약을 관리하는 서비스를 사용중이다.&lt;/p&gt;
&lt;p&gt;이 서비스에서 예약한 회의 30분 전에 회의 참가자들에게 알림을 주고 싶다는 요구사항이 생겼다.&lt;/p&gt;
&lt;p&gt;외부에서 제작한 서비스이기 때문에 직접 서비스에 기능을 추가할 수 없는 상황이었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다만 서비스에서 회의실 예약 정보를 저장하고 있는 DB의 테이블과 자료의 형식은 알고있었다.&lt;/p&gt;
&lt;p&gt;그래서 해당 테이블을 감시하면서 다가온 회의실 예약에 대해 알림을 제공하는 프로그램을 만들기로 하였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예약 정보는 DB에서 가져왔지만, 이걸 알림을 주려면 뭘로 해야할지 고민이 되었다.&lt;/p&gt;
&lt;p&gt;쉽게는 텔레그램 같은 메신저로 보내도 되고, 사내에서 사용하는 로켓챗을 이용해도 된다.&lt;/p&gt;
&lt;p&gt;여러가지를 고민하다가 구글캘린더에 일정으로 등록해주면 이 예약정보를 다양하게 활용이 가능하겠다는 생각을 하게 되었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;사용자가 자신의 일정앱에서 일정 시작 몇분전에 알림을 줄지 정할 수 있다(기본값은 30분, 10분)&lt;/li&gt;
&lt;li&gt;휴대폰의 일정앱 뿐 아니라 컴퓨터의 브라우저에서도 일정 기능을 키면 확인이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;구글캘린더를 도입 하려면 알림을 받을 사람들의 구글계정이 필요하다.&lt;/p&gt;
&lt;p&gt;요즘 대부분 구글계정 한개씩 가지고 있기때문에 큰 제약은 아니었다.&lt;/p&gt;
&lt;p&gt;이 점만 합의가 된다면 원래 요건을 충족하고 추가로 편의성을 더 보장할 수 있어서 구글캘린더에 회의실 예약 정보를 등록하기로 하였다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구글캘린더 API 와 연동하려면 구글개발자 계정과 API 등록과정이 필요하다.&lt;/p&gt;
&lt;p&gt;또한 JAVA에서 OAuth2 인증을 위해 인증정보를 담은 json 파일이 필요하다.&lt;/p&gt;
&lt;p&gt;설정과정은 정리가 잘 된 링크를 첨부한다. 아래 링크에서 json 파일을 다운받는 곳 까지 진행하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kingbbode.tistory.com/8&quot;&gt;https://kingbbode.tistory.com/8&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;개발은 SpringBoot 2, Maven 환경에서 진행하였다.&lt;/p&gt;
&lt;p&gt;연동부분에 대한 코드는 딱히 스프링의 라이브러리를 사용하지 않았으니 스프링이 꼭 필요하지는 않다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구글 oauth 인증을 하기 위해서는 아래 라이브러리 디펜던시를 추가 해야한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1555649340314&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;com.google.api-client&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;google-api-client&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;1.23.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;com.google.oauth-client&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;google-oauth-client-jetty&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;1.23.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
   &amp;lt;groupId&amp;gt;com.google.apis&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;google-api-services-calendar&amp;lt;/artifactId&amp;gt;
   &amp;lt;version&amp;gt;v3-rev305-1.23.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래는 구글캘린더 API 를 이용해 일정을 등록하고 삭제하는 내용을 담은 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1555649432948&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class GoogleCalendar {



   private static final String APPLICATION_NAME = &quot;Google Calendar API Java Quickstart&quot;;

   private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

   private static final String CREDENTIALS_FOLDER = &quot;credentials&quot;; // Directory to store user credentials.

   private static final String CALENDAR_ID = &quot;[캘린더연동아이디]&quot;;



   /**

    * Global instance of the scopes required by this quickstart.

    * If modifying these scopes, delete your previously saved credentials/ folder.

    */

   private static final List&amp;lt;String&amp;gt; SCOPES = Collections.singletonList(CalendarScopes.CALENDAR);

   private static final String CLIENT_SECRET_DIR = &quot;/client_secret.json&quot;;





   public static Event addEvent(Event event) throws IOException, GeneralSecurityException, GoogleJsonResponseException {

       final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

       Calendar service = new Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))

               .setApplicationName(APPLICATION_NAME)

               .build();

       return service.events().insert(CALENDAR_ID, event).execute();

   }



   public static void delEvent(String eventKey) throws IOException, GeneralSecurityException, GoogleJsonResponseException {

       final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();

       Calendar service = new Calendar.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))

               .setApplicationName(APPLICATION_NAME)

               .build();

       service.events().delete(CALENDAR_ID, eventKey).execute();

   }



   /**

    * Creates an authorized Credential object.

    * @param HTTP_TRANSPORT The network HTTP Transport.

    * @return An authorized Credential object.

    * @throws IOException If there is no client_secret.

    */

   private static Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException {

       // Load client secrets.

       InputStream in = GoogleCalendar.class.getResourceAsStream(CLIENT_SECRET_DIR);

       GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));



       // Build flow and trigger user authorization request.

       GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(

               HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)

               .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(CREDENTIALS_FOLDER)))

               .setAccessType(&quot;offline&quot;)

               .build();

       return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize(&quot;user&quot;);

   }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;b&gt;getCredentials&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;OAuth 인증을 이용해 구글에서 인증을 가져오는 메소드이다.&lt;/p&gt;
&lt;p&gt;앞에서 만든 인증 json을 불러와야 한다. 나의 경우는 resource 폴더에 넣고 불러왔다.&lt;/p&gt;
&lt;p&gt;이 인증메소드는 api 요청 때 마다 실행해서 인증정보를 반환한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;&lt;b&gt;addEvent&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;구글캘린더에 일정을 추가하는 메소드이다.&lt;/p&gt;
&lt;p&gt;일정정보를 담기 위해 Event 라는 객체를 사용한다.&lt;/p&gt;
&lt;p&gt;Event 객체를 사용하는 방법은 아래에 따로 다루었으니 참고하자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;&lt;b&gt;delEvent&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;구글캘린더에 일정을 삭제하는 메소드이다.&lt;/p&gt;
&lt;p&gt;addEvent 했을때 구글에서 응답받은 eventKey 가 필요하다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;&lt;b&gt;Event 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;이벤트 생성은 크게 제목, 시작시간, 종료시간, 참여자 정보가 필요하다. 아래는 일정을 추가하는 메소드이다.&lt;/p&gt;
&lt;p&gt;참여자정보(ResourceSubscriber), 일정정보(RecourceInfo), 사원정보(EmpInfo) 는 나의 시스템에서 사용하는 DB에 맞게 직접 만든 객체이다. 참여자정보, 일정정보, 사원정보 부분은 본인의 시스템에 맞게 각자 제작하면 된다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1555649405363&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Event makeEvent(ResourceInfo resourceInfo, List&amp;lt;ResourceSubscriber&amp;gt; subList) throws ParseException, IOException, GeneralSecurityException {

   Event event = new Event()

           .setSummary(resourceInfo.getReqText())

           .setLocation(getMeetingRoom(resourceInfo.getResSeq()).getResName()) //장소이름은 따로 불러오길..

           .setDescription(resourceInfo.getDescText());



   DateTime startDateTime = new DateTime(dateTimeTzFormat.format(dateTimeFormat.parse(resourceInfo.getStartDate())));

   EventDateTime start = new EventDateTime()

           .setDateTime(startDateTime)

           .setTimeZone(&quot;Asia/Seoul&quot;);

   event.setStart(start);



   DateTime endDateTime = new DateTime(dateTimeTzFormat.format(dateTimeFormat.parse(resourceInfo.getEndDate())));

   EventDateTime end = new EventDateTime()

           .setDateTime(endDateTime)

           .setTimeZone(&quot;Asia/Seoul&quot;);

   event.setEnd(end);



   EventReminder[] reminderOverrides = new EventReminder[]{

           new EventReminder().setMethod(&quot;popup&quot;).setMinutes(10),

   };

   Event.Reminders reminders = new Event.Reminders()

           .setUseDefault(false)

           .setOverrides(Arrays.asList(reminderOverrides));

   event.setReminders(reminders);



   List&amp;lt;EventAttendee&amp;gt; attendList = new ArrayList();



   for (ResourceSubscriber subObj : subList) {

       logger.info(&quot;SUB ADD OBJ : &quot; + subObj.toString());

       EmpInfo empInfo = getEmpInfo(subObj.getEmpSeq());



       if (empInfo.getOutMail() != null &amp;amp;&amp;amp; empInfo.getOutMail().length() &amp;gt; 0 &amp;amp;&amp;amp; empInfo.getOutDomain() != null &amp;amp;&amp;amp; empInfo.getOutDomain().length() &amp;gt; 0) {

           if (empInfo.getOutDomain().equals(&quot;gmail.com&quot;)) {

               attendList.add(new EventAttendee().setEmail(empInfo.getOutMail() + &quot;@&quot; + empInfo.getOutDomain()));

           } else {

               //이메일주소가 gmail.com 이 아닌 케이스. 일단은 아무것도 안함.

           }

       } else {

           //메일주소가 없는 케이스.

       }

   }

   event.setAttendees(attendList);

   try {

       event = GoogleCalendar.addEvent(event);

   } catch (Exception ex) {

       logger.info(&quot;Calendar Exception in insert&quot;);

   }

   logger.info(&quot;EVENT : &quot; + event.toPrettyString());



   return event;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구글 캘린더 API 에는 더 다양한 기능이 있다. 아래 링크에서 참고하자.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developers.google.com/calendar/v3/reference/&quot;&gt;https://developers.google.com/calendar/v3/reference/&lt;/a&gt;&lt;/p&gt;
&lt;figure class=&quot;og-loading&quot; contenteditable=&quot;false&quot;&gt;불러오는 중입니다...&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/47</guid>
      <comments>https://team-platform.tistory.com/47#entry47comment</comments>
      <pubDate>Fri, 19 Apr 2019 13:52:41 +0900</pubDate>
    </item>
    <item>
      <title>KoNLPy  (코엔엘파이)</title>
      <link>https://team-platform.tistory.com/46</link>
      <description>&lt;p&gt;자연어 처리에서 각 언어마다 모두 특징이 다르기 때문에 동일한 방법을 사용하기는 어려울 것이다. 한글에도 NLTK나 Spacy 같은 도구를 사용할 수 있으면 좋겠지만 언어 특성상 영어를 위한 도구를 사용하기에는 적합하지 않다. 하지만 많은 사람들의 노력으로 개발된 한글 자연어 처리를 돕는 훌륭한 도구를 사용할 수있다. 그중 한글 자연어 처리에 많이 사용하는 파이썬 라이브버리 KoNLPy에 대해 알아보겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;konlpy.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bA0Dte/btquDWPakhw/k80ylswPcfghh7cggRIZbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bA0Dte/btquDWPakhw/k80ylswPcfghh7cggRIZbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bA0Dte/btquDWPakhw/k80ylswPcfghh7cggRIZbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbA0Dte%2FbtquDWPakhw%2Fk80ylswPcfghh7cggRIZbk%2Fimg.png&quot; data-filename=&quot;konlpy.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;KoNLPy는 한글 자연어 처리를 쉽고 간결하게 처리할 수 있도록 만들어진 오픈소스 라이브러리다. 또한 국내에 이미 만들어져 사용되고 있는 여러 형태소 분석기를 사용할 수 있게 허용한다. 일반적인 어절 단위에 대한 토크나이징은 NLTK로 충분히 해결할 수 있으므로 형태소 단위에 대한 토크나이징에 대해 알아보도록 하겠다.&lt;/p&gt;
&lt;h1 id=&quot;설치&quot;&gt;설치&lt;/h1&gt;
&lt;p&gt;리눅스 또는 macOS에서는 다음과 같이 pip를 이용해 간단하게 설치할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1555595957093&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install konlpy #python2.x
pip3 install konlpy #python2.x&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;형태소-단위-토크나이징&quot;&gt;형태소 단위 토크나이징&lt;/h1&gt;
&lt;p&gt;한글 텍스트의 경우 형태소 단위 토크나이징이 필요할 떄가 있는데 KoNLPy에서는 여러 형태소 분석기를 제공하며, 각 형태소 분석기별로 분석한 결과는 다를 수 있다. 각 형태소 분석기는 클래스 형태로 되어 있고 이를 객체로 생성한 후 매서드를 호출해서 토크나이징할 수 있다.&lt;/p&gt;
&lt;h1 id=&quot;형태소-분석-및-품사-태깅&quot;&gt;형태소 분석 및 품사 태깅&lt;/h1&gt;
&lt;p&gt;형태소란 의미를 가지는 가장 작은 단위로서 더 쪼개지면 의미를 상실하는 것들을 말한다. 따라서 형태소 분석이란 의미를 가지는 단위를 기준으로 문장을 살펴보는 것을 의미한다. KoNLPy는 기존에 C, C++, Java 등의 언어를 통해 형태소 분석을 할 수 있는 좋은 라이브러리들을 파이썬 라이브러리로 통합해서 사용할 수 있록 하여 한국어 구문 분석을 쉽게 할 수 있도록 만들어진 라이브러리이다. KoNLPy에는 다양한 형태소 분석기들이 객체 형태로 포함돼 있으며 다음과 같은 각 형태소 분석기 목록이 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hannanum&lt;/li&gt;
&lt;li&gt;Kkma&lt;/li&gt;
&lt;li&gt;Komoran&lt;/li&gt;
&lt;li&gt;Mecab&lt;/li&gt;
&lt;li&gt;Okt(Twitter)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;모두 동일한 형태소 분석기능을 제공하는데, 각기 성능이 조금씩 다르다고 하니 직접 비교해보고 자신의 데이터를 가장 잘 분석하는 분석기를 사용하는 것이 좋다. (단, Mecab는 윈도우에서 사용할 수 없다.)&lt;/p&gt;
&lt;p&gt;여기에서는 Okt 예로 들어 설명 하도록 하겠다. Okt는 원래 이름이 Twitter였으나 0.5.0 버전 이후부터 이름이 Okt 바뀌었다.&lt;/p&gt;
&lt;pre id=&quot;code_1555595987955&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import konlpy.tag import Okt okt = Okt() # 객체 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Okt 에서 제공되는 함수를 살펴보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;okt.morphs() 텍스트를 형태소 단위로 나눈다. 옵션으로 norm과 stem이 있다. norm은 문장을 정규화. stem은 각 단어에서 어간을 추출.(기본값은 둘다 False)&lt;/li&gt;
&lt;li&gt;okt.nouns() 텍스트에서 명사만 뽑아낸다.&lt;/li&gt;
&lt;li&gt;okt.phrases() 텍스트에서 어절을 뽑아낸다.&lt;/li&gt;
&lt;li&gt;okt.pos() 각 품사를 태깅하는 역할을 한다. 품사를 태깅한다는 것은 주어진 텍스트를 형태소 단위로 나누고, 나눠진 각 형태소를 그에 해당하는 품사와 함께 리스트화하는 것을 의미한다. 옵션으로 norm, stem, join이 있는데 join은 나눠진 형태소와 품사를 &amp;lsquo;형태소/품사&amp;rsquo; 형태로 같이 붙여서 리스트화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음 문장을 직접 각 함수에 적용해서 살펴보자.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;모바일 게임은 재밌다 열심히 해서 만랩을 찍어야지~ ㅎㅎㅎ&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1555596030676&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from konlpy.tag import Okt
okt = Okt()

text = &quot;모바일 게임은 재밌다 열심히 해서 만랩을 찍어야지~ ㅎㅎㅎ&quot;

print(okt.morphs(text))
print(okt.morphs(text, stem=True))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt;['모바일', '게임', '은', '재밌다', '열심히', '해서', '만', '랩', '을', '찍어야지', '~', 'ㅎㅎㅎ'] &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt;['모바일', '게임', '은', '재밌다', '열심히', '하다', '만', '랩', '을', '찍다', '~', 'ㅎㅎㅎ']&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;어간 추출을 한 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;찍어야지의 어간인&lt;span&gt;&amp;nbsp;&lt;/span&gt;찍다로 추출된 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;이제 명사와 어절을 추츨헤 보자&lt;/p&gt;
&lt;pre id=&quot;code_1555596102860&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;print(okt.nouns(text))
print(okt.phrases(text))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt;['모바일', '게임', '랩']&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt; ['모바일', '모바일 게임', '만랩', '게임']&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;nouns 함수를 사용한 경우에는 명사만 추출되었고 phrases 함수의 경우 어절 단위로 나뉘어서 추출 되었다.&lt;/p&gt;
&lt;p&gt;품사 태깅을 하는 함수 pos를 사용해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1555596130810&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;print(okt.pos(text))
print(okt.pos(text, join=True))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt;[('모바일', 'Noun'), ('게임', 'Noun'), ('은', 'Josa'), ('재밌다', 'Adjective'), ('열심히', 'Adverb'), ('해서', 'Verb'), ('만', 'Modifier'), ('랩', 'Noun'), ('을', 'Josa'), ('찍어야지', 'Verb'), ('~', 'Punctuation'), ('ㅎㅎㅎ', 'KoreanParticle')]&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt; ['모바일/Noun', '게임/Noun', '은/Josa', '재밌다/Adjective', '열심히/Adverb', '해서/Verb', '만/Modifier', '랩/Noun', '을/Josa', '찍어야지/Verb', '~/Punctuation', 'ㅎㅎㅎ/KoreanParticle']&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;join 옵션을 True로 설정 하면 형태소와 품사가 함께 나오는 것을 볼 수 있다. 경우에 따라 옵션을 설정하면서 사용하면 된다.&lt;/p&gt;
&lt;h1 id=&quot;konlpy-데이터&quot;&gt;KoNLPy 데이터&lt;/h1&gt;
&lt;p&gt;KoNLPy 라이브러리는 한글 자연어 처리에 활용할 수 있는 한글 데이터를 포함하고 있어 라이브러리를 통해 데이터를 바로 사용할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kolaw 한국 법률 말뭉치. &amp;lsquo;constitution.txt&amp;rsquo;파일&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;kobill 대한민국 국회 의안 말뭉치. 각 id값을 가지는 의안으로 구성. 파일은 &amp;lsquo;1809890.txt&amp;rsquo; 부터 &amp;lsquo;1809899.txt&amp;rsquo;까지로 구성.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라이브러리를 사용해 각 말뭉치를 불러오자.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1555596273462&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from konlpy.corpus import kolaw
from konlpy.corpus import kobill

kolaw.open('constitution.txt').read()[:30]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt;'대한민국헌법\n\n유구한 역사와 전통에 빛나는 우리 대한국'&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1555596322710&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kobill.open('1809890.txt').read()[:30]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #ff7600;&quot;&gt;'지방공무원법 일부개정법률안\n\n(정의화의원 대표발의 )\n'&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 데이터들을 가지고 여러 가지 한글 자연어 처리 문제를 연습하는 데 활용할 수 있다.&lt;/p&gt;
&lt;h1 id=&quot;마치며&quot;&gt;마치며&lt;/h1&gt;
&lt;p&gt;KoNLPy 홈페이지에 가보면 나름의 철학을 가지고 프로젝트를 진행하는 듯한 글을 볼 수 있다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;KoNLPy는 같은 기능을 하는 또 하나의 도구를 만들려는 것이 아닙니다. 이 프로젝트에는 세 가지 철학이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용법이 간단해야 한다.&lt;/li&gt;
&lt;li&gt;누구나 쉽게 이용할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;인터넷 민주주의는 효과적이다.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;개인적으로 마음에 드는 글귀이다. 인터넷 민주주의를 위해 직접 참여도 가능하니 한국어 NLP에 관심이 많다면 아래 공식홈을 방문하여 살펴보길 바란다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;KoNLPy 공식홈 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://konlpy-ko.readthedocs.io&quot;&gt;https://konlpy-ko.readthedocs.io&lt;/a&gt;&lt;/p&gt;</description>
      <category>자연어처리 #konlpy #형태소분석</category>
      <author>@위너스</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/46</guid>
      <comments>https://team-platform.tistory.com/46#entry46comment</comments>
      <pubDate>Thu, 18 Apr 2019 23:10:04 +0900</pubDate>
    </item>
    <item>
      <title>Docker : Docker Compose 편</title>
      <link>https://team-platform.tistory.com/45</link>
      <description>&lt;p&gt;지난 도커파일 편에서 원하는 기능이 담긴 이미지를 손쉽게 만들 수 있는 도커파일의 작성방법과 이미지 생성, 컨테이너 생성 실습을 진행 했었다. 이번 Docker Compose(이하 도커 컴포즈) 편 에서는 도커 컴포즈의 개요와 YAML파일의 작성 및 실습을 진행 해보도록 하겠다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Docker Compose 개요&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;웹서버 기능을 하는 어플리케이션이 있다고 가정 해보자 여기에는 DB와 Web Server가 필요하고 이것을 각각의 컨테이너로 나누었다. 제대로 동작 하는지 확인 해보기 위해서는 각 컨테이너들을 실행 시켜야 하는데 docker run 명령어를 이용하여 컨테이너를 생성하려면 두 번의 run 명령어 입력이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;img01.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4TDrg/btqt7RvbLfU/pNrENPfwVCSPF0JTw4vxyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4TDrg/btqt7RvbLfU/pNrENPfwVCSPF0JTw4vxyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4TDrg/btqt7RvbLfU/pNrENPfwVCSPF0JTw4vxyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4TDrg%2Fbtqt7RvbLfU%2FpNrENPfwVCSPF0JTw4vxyk%2Fimg.png&quot; data-filename=&quot;img01.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;컨테이너를 가끔 생성하는 경우라면 괜찮겠지만 컨테이너의 생성빈도가 높고 실행 시켜야하는 컨테이너가 지금보다 더 늘어난다면 상당히 번거로운 작업이 된다. 이런 경우 여러개의 컨테이너를 한번에 실행 시키고 관리 할 수 있다면 효율적일 것이다. 도커 컴포즈는 위와 같이 여러 컨테이너를 한번에 관리 할 때 아주 유용하다. YAML(확장자 *.yml) 파일을 이용하여 어떠한 이미지를 사용하여 어떤 컨테이너를 어떻게 실행 시킬 것인지 기술해주면 도커는 해당 내용대로 컨테이너를 순차적으로 실행 시킨다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Docker Compose YAML 파일 작성&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 한 어플리케이션의 구성요소인 MariaDB, Apache를 일반적으로 컨테이너화 할 경우 보통은 아래와 같이 run 명령어를 사용하여 이미지를 내려받고 컨테이너를 실행 시킬 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1554450562487&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[root@localhost testuser]# docker run \
--name mdb \
-e MYSQL_ROOT_PASSWORD='1234' \
-d mariadb:latest
da6943f279767932259d28dfd6d0f92cb3c90499569c3907a6e08594e58b8b54

[root@localhost testuser]# docker run \
--name web \
-it \
-p 80:80 \
--link mdb:mdb docker_img
 * Restarting OpenBSD Secure Shell server sshd
 root@2ae7ab2da8df:/#&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 두개의 run 명령어를 YAML 파일로 작성해보자. (※주의 : 탭 인식 못하므로 스페이스 두칸으로 구분)&lt;/p&gt;
&lt;pre id=&quot;code_1554450562487&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'
services:
  mdb:
    image: mariadb:latest
    environment:
      MYSQL_ROOT_PASSWORD: 1234
  web:
    build: ./dockerfile
    image: apache_df:web
    ports:
      - &quot;80:80&quot;
    links:
      - mdb:mdb&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;version : YAML파일 포멧의 버전을 의미하고 여기서는 3버전을 사용하였다.&lt;/p&gt;
&lt;p&gt;services : 생성될 컨테이너의 정보를 담고 있다. services 바로 아래에 서비스명을 기술 할 수 있으며 위에 기술 된 mdb, web이 서비스명이 된다.&lt;/p&gt;
&lt;p&gt;image : docker images 명령어를 실행하면 노출되는 repository명이다.&lt;/p&gt;
&lt;p&gt;environment :&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;docker run 명령어 옵션&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;-e와 같으며 위에 기술한 MYSQL_ROOT_PASSWORD와 같이 컨테이너 생성 시 들어갈 환경 변수를 지정 해줄 수 있다.&lt;/p&gt;
&lt;p&gt;build : 지정된 경로 내에 존재하는 도커파일을 실행하여 이미지로 만들고 그 이미지로 컨테이너를 생성한다.&lt;/p&gt;
&lt;p&gt;ports : docker run 명령어 옵션 -p와 같으며 해당 컨테이너 내에서 오픈 할 포트번호를 지정 할 수 있다.&lt;/p&gt;
&lt;p&gt;links :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;docker run 명령어 옵션 --link와 같으며 연결할&lt;/span&gt;&amp;nbsp;서비스명을 입력하여 해당 서비스로 접근 할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Docker Compose 실행&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;YAML 파일 작성이 완료 되었으면 도커 컴포즈 파일을 docker-compose.yml이라는 이름으로 저장해준다. 필자는&amp;nbsp;/home/testuser/docom 경로에 저장하였다. 도커 컴포즈 실행 전 실습을 위해 이전에 만들었던 컨테이너들을 모두 지워주도록 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1554450562491&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker rm -f $(docker ps -a -q)
2ae7ab2da8df
da6943f27976
# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이어서 도커 컴포즈를 다운로드 받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1554450562491&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   617    0   617    0     0     98      0 --:--:--  0:00:06 --:--:--   163
100 15.4M  100 15.4M    0     0   754k      0  0:00:20  0:00:20 --:--:-- 1840k
# sudo chmod +x /usr/local/bin/docker-compose
# ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
# docker-compose --version
docker-compose version 1.24.0, build 0aa59064&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다운로드가 완료 되었으면 위에 작성한 YAML파일을 기반으로 도커 컴포즈를 실행 시켜보자.&lt;/p&gt;
&lt;p&gt;Apache2는 미리 기술해둔 dockerfile을 통해 image를 생성하였다. 그리고 해당 이미지를 통해 docom_web_1 이라는 컨테이너를 생성한 것을 확인하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1554450562491&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# pwd
/home/testuser/docom
# docker-compose up -d
Creating network &quot;docom_default&quot; with the default driver
Building web
Step 1/6 : FROM ubuntu:14.04
 ---&amp;gt; 5dbc3f318ea5
Step 2/6 : MAINTAINER pamtrak06 &amp;lt;pamtrak06@gmail.com&amp;gt;
 ---&amp;gt; Using cache
 ---&amp;gt; 9c561b8834f8
Step 3/6 : RUN apt-get update &amp;amp;&amp;amp; apt-get install -y apache2 apache2-threaded-dev
 ---&amp;gt; Using cache
 ---&amp;gt; 09fa1cec80dc
Step 4/6 : RUN echo &quot;ServerName localhost&quot; &amp;gt;&amp;gt; /etc/apache2/apache2.conf
 ---&amp;gt; Using cache
 ---&amp;gt; ff1eee1b0bdb
Step 5/6 : CMD apachectl -D FOREGROUND
 ---&amp;gt; Using cache
 ---&amp;gt; e8563809e2c6
Step 6/6 : EXPOSE 80
 ---&amp;gt; Using cache
 ---&amp;gt; d307433464d8
Successfully built d307433464d8
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating docom_mdb_1 ... done
Creating docom_web_1 ... done&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;docker ps -a 명령을 통해 컨테이너가 제대로 생성 되었는지 다시 한번 확인 해보고 이번 편을 마치도록 하겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1554450562491&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[root@localhost docom]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
044eba02fb3c        apache_df:web       &quot;apachectl -D FORE...&quot;   11 seconds ago      Up 10 seconds       0.0.0.0:80-&amp;gt;80/tcp   docom_web_1
ede97239d9bc        mariadb:latest      &quot;docker-entrypoint...&quot;   11 seconds ago      Up 11 seconds       3306/tcp             docom_mdb_1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;컨테이너가 잘 생성되었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 Docker : Docker Compose 편 에서는 여러개의 컨테이너를 도커 컴포즈를 이용하여 하나로 묶는 개념으로 다소 복잡할 수 있는 분산된 컨테이너를 효과적으로 관리할 수 있도록 YAML파일을 기술하고 실습해보았다. 특히 Dockerfile을 같이 활용하여 사용 할 경우 언제 어느 환경에서도 즉시 컨테이너를 생성하고 관리 할 수 있게 되었다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;b&gt;Docker : Docker Compose 편&lt;/b&gt;&lt;/h3&gt;
&lt;h3&gt;&lt;b&gt;끝.&lt;/b&gt;&lt;/h3&gt;</description>
      <category>Article</category>
      <author>알 수 없는 사용자</author>
      <guid isPermaLink="true">https://team-platform.tistory.com/45</guid>
      <comments>https://team-platform.tistory.com/45#entry45comment</comments>
      <pubDate>Fri, 5 Apr 2019 16:49:55 +0900</pubDate>
    </item>
  </channel>
</rss>