[ { "title": "Jekyll 테마에서 구글 검색 노출 등록하기", "url": "/posts/jekyll-google-search/", "categories": "jekyll", "tags": "jekyll, chirpy, google, google search, search engine, 검색노출, 구글검색, sitemap.xml, sitemap, 사이트맵", "date": "2022-01-13 15:11:33 +0900", "snippet": " 구글에 검색을 통해 사이트로 인입되는 비율은 엄청납니다. 우리나라는 naver의 영향력이 막강하지만, 외부의 블로그나 사이트들은 그래도 구글을 통한 인입이 강력한 편입니다. jekyll 테마에서 구글 검색엔진에 포스트 글들이 검색되도록 설정해 보겠습니다.chirpy 기준이기는 하나 기본이 같기 때문에 쉽게 적용할 수 있을 겁니다. 참고한 사이트가 너무 잘 정리되어 있으나, 끝부분이 저와 환경이 달라 재정리 합니다.앞부분은 거의 똑같습니다.(카피라고 뭐라한다면… 음.. 내리겠습니다… 😭)준비하기우선 구글 계정이 필요합니다. 없다면 아래 링크를 통해 구글 계정을 새로 만듭니다. 구글 계정구글 웹마스터 도구 등록하기시작하기구글 웹마스터 도구(Search Console)에 접속합니다. 그러면 아래와 같이 화면이 나타나고, 시작하기 버튼을 클릭합니다.URL 입력하기속성유형 선택하는 화면에서 URL접두어를 사용하여 사이트 주소를 입력하고 계속 버튼을 클릭합니다.다른 확인 방법 선택하기소유권 확인하기 위한 창이 뜨게 되면 다른 확인 방법에서 HTML 태그 항목을 선택합니다. jekyll에서 header부분을 세팅할 수 있기 때문에 이 방법이 유용하네요.메타태그 복사하기메타태그를 복사를 합니다. 아직 확인을 누르지 마세요. 지금 단계여서 확인을 하게 되면 소유권 확인 실패 경고창이 뜨게 될 것입니다.복사가 됐으면 브라우저 창을 닫지 말고 내용을 먼저 확인합니다. 복사한 내용을 텍스트 편집기에서 붙여넣어 보면 다음과 같은 형태로 나옵니다.&amp;lt;meta name=&quot;google-site-verification&quot; content=&quot;1gDBG.........................................._stqA&quot; /&amp;gt;여기서 content 부분이 소유권 확인용 코드입니다. 보통 jekyll 블로그에서는 header 부분에 내용 전체를 넣어두면 되는데요. Chirpy에서는 _config.yml에 설정하는 항목이 있습니다._config.yml에 등록하기_config.yml에 google_site_verification 항목에 위에서 복사한 메타태그에서 content 부분을 넣어 줍니다....google_site_verification: 1gDBG.........................................._stqA...git push를 한 후 deploy가 끝날 때까지 잠시 기다립니다.소유권 확인하기다시 메타태그를 복사했던 브라우저 창으로 돌아와서 확인 버튼을 클릭합니다. 구글에서 인증을 시도하게 되고, 아래와 같은 창이 뜨게 되면 성공한 것입니다.sitemap.xml 등록하기블로그가 만들어지면 github이 자동으로 sitemap.xml을 만들어 줍니다. 그러니 다른 작업은 필요없습니다. 그러나 수정을 하고 싶다면, 플러그인 없이 Jekyll Sitemap 만들기를 참고해서 sitemap.xml 파일을 만들어 주면 됩니다.기본 구성요소는 sitemap xml 형식을 참조하면 되겠네요.준비가 되면 Google Search Console &amp;gt; Sitemaps &amp;gt; 새 사이트맵 추가 부분에 sitemap.xml을 입력하고 제출버튼을 클릭합니다.아래와 같이 나오게 되면 정상적으로 등록 완료된 것입니다.결과 확인모든 절차가 정상적으로 진행됐다고 바로 구글 검색에 노출되는 것은 아닙니다. 짧게는 하루, 길게는 1주일 정도 걸리는 것 같습니다.저는 3일 정도 걸린 것 같습니다.(날짜 계산을 안해봐서.. 😏)아래와 같이 결과가 잘 나오네요.문제 해결 구글 Search Console에서 sitemap.xml을 가져오지 못할 때 미안하지만, 버그입니다. 아직도 수정이 안된 것 같습니다. 다른 이유는 모르겠고, 해결이 안됩니다. 😱헌데, 이건 화면상의 버그이고, 실제는 정상적으로 동작합니다. 단, “사이트맵을 읽을 수 없음” 이라고 나오면서 다른 메세지가 없어야 하며, 오른쪽 상단에 “사이트맵 열기” 버튼을 클릭해서 정상적으로 보인다면 문제없는 것 같습니다.참고 : Sitemap could not be read in new GSC 참조 Jekyll sitemap plugin 구글 사이트 등록하기(Google 검색 노출) 구글 Search Console sitemap xml 형식 플러그인 없이 Jekyll Sitemap 만들기" }, { "title": "컴퓨터 한대로 github 여러 계정 사용하기", "url": "/posts/github-%EC%BB%B4%ED%93%A8%ED%84%B0-%ED%95%9C%EB%8C%80%EB%A1%9C-%EC%97%AC%EB%9F%AC-%EA%B3%84%EC%A0%95-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0/", "categories": "tools, git", "tags": "git, github, 여러 계정, multi account, ssh, ssh-add, ssh-keygen", "date": "2021-12-29 12:15:33 +0900", "snippet": " 컴퓨터 한 대에서 github 계정을 여러개 사용하다 보면 불편한 것이 이만저만이 아닙니다.ssh를 이용해서 host를 변경하고 키를 자동으로 인식하게 하면 너무나 편리하게 여러 계정을 사용할 수 있군요.여러 계정 설정하기ssh를 사용하여 github을 사용합니다. 계정별로 ssh key를 만들고 자동 로그인하도록 설정할 것이기 때문입니다.ssh에 대해 자세히 보시려면 SSH Home Page를 참고하세요~적용하는 절차는 다음과 같습니다. ssh key 생성 ssh config 설정 ssh agent에 등록 github에 public key 등록 git clone 받기ssh key 생성 홈디렉토리 아래에 .ssh 디렉토리를 생성합니다. 반드시 앞에 .을 붙여야 합니다. 생성한 .ssh 디렉토리로 이동합니다. ssh-keygen -t rsa -C [github 이메일 계정] -f [생성될 key 파일] 형식으로 키를 생성합니다.$ mkdir ~/.ssh$ cd ~/.ssh$ ssh-keygen -t rsa -C &quot;git1@email.com&quot; -f &quot;git1&quot;Generating public/private rsa key pair.Enter passphrase (empty for no passphrase): &amp;lt;- 여기서 그냥 엔터를 칩니다. 패스워드 입력을 하지 않아도 됩니다.Enter same passphrase again: &amp;lt;- 여기서 그냥 엔터를 칩니다. 패스워드 입력을 하지 않아도 됩니다.Your identification has been saved in git1Your public key has been saved in git1.pubThe key fingerprint is:SHA256:OJPEtuo4rR+O2b2iR/c0aomeMw3M/x+NDHUSSeqGCGM git1@email.comThe key&#39;s randomart image is:+---[RSA 3072]----+| .o. || . ... || E + . o . || . o + * . o || o. B S || =..+oo o || oo* = .+ . || .BB+* . . || *BB=oo... |+----[SHA256]-----+위와 같이 메세지가 나타나면 잘 만들어 진 것입니다.생성된 파일을 확인해 봅시다.$ ls -ltotal 16-rw------- 1 iam geni 2602 12 29 13:44 git1-rw-r--r-- 1 iam geni 568 12 29 13:44 git1.pub위와 같이 두개의 파일이 생성되어 있습니다. git1은 private key이고, git1.pub은 public key입니다. 잘 생성되었다면, 2번도 만들어 봅시다.$ ssh-keygen -t rsa -C &quot;git2@email.com&quot; -f &quot;git2&quot;첫번째와 동일한 방법으로 만듭니다. 패스워드는 생성하지 맙시다~ 귀찮아져요.😅 다시 파일 목록을 보면 git2, git2.pub 파일도 만들어져 있을 겁니다.ssh config 설정텍스트 편집기를 사용하여 config파일을 만들고 안에 아래 내용을 입력합니다.Host github.com-git1 HostName github.com User git1@email.com IdentityFile ~/.ssh/git1Host github.com-git2 HostName github.com User git2@email.com IdentityFile ~/.ssh/git2위 내용을 보시면, 두개의 새로운 host를 만들었습니다. 첫번째는 github.com-git1이고, 두번째는 github.com-git2입니다. ssh 사용 시 Host명을 이렇게 주게 되면 해당하는 HostName으로 접속하고, User 부분의 계정을 사용하며, 인증키로 IdentifyFile을 참조하게 됩니다.따라서, 두개의 새로운 Host를 만들고 접속하는 계정을 이곳에서 설정하는 것이 되겠습니다.ssh agent에 등록생성된 key는 ssh agent에 등록을 해 줘야 동작합니다. 아래와 같이 두개를 모두 등록해 줍시다.$ ssh-add git1Identity added: git1 (git1@email.com)$ ssh-add git2Identity added: git2 (git2@email.com)두개가 잘 등록 되었군요. agent에 등록된 내용을 다시 확인해 봅시다$ ssh-add -l3072 SHA256:OJPEtuo4rR+O2b2iR/c0aomeMw3M/x+NDHUSSeqGCGM git1@email.com (RSA)3072 SHA256:Nyn0lufvrcYw2sMSMEcE1/uA7D2eQKD9ljltcJCdI1M git2@email.com (RSA)음.. 뭔가 복잡한 것이 나오지만, 뒤에 제가 입력한 이메일이 잘 보이는 군요.이제 터미널에서 할 작업은 끝났습니다. 컴퓨터를 리붓해도 계속 적용될 것입니다. 참고 : 리붓 후에 ssh-add -l명령으로 확인해 보면 아무것도 안 나옵니다. 그러나 잘 작동합니다.github에 public key 등록git1.pub 파일을 열어보면 이상한 문자들이 잔뜩 있습니다. 이를 모두 복사해 둡니다.github에 로그인하여 아래 그림처럼 Settings &amp;gt; SSH and GPG keys를 클릭하고 들어갑니다.그러면 아래와 같이 화면이 나오게 되고, New SSH key 버튼을 클릭합니다.Key 부분에 앞에서 복사한 public key를 붙여넣어 줍니다. title은 아무거나 입력해도 됩니다.key는 반드시 .pub 확장자가 붙어 있는 파일의 내용 전체를 복사해서 넣어줍니다. ssh-rsa로 시작해서 내가 설정한 이메일로 끝나게 되어 있습니다. 이제 Add SSH Key 버튼을 클릭하면 등록 절차가 모두 끝나게 됩니다.git clone 받기모든 설정이 끝났으니 두 계정으로 소스를 다운받아 봅시다.clone 받을 때 프로토콜을 HTTPS에서 SSH로 변경해 줍니다. 그리고 복사를 합니다.터미널을 열어서 소스를 클론 받습니다. 기본 경로가 git@github.com:[user]/[저장소] 이렇게 표시됩니다.여기서 git@github.com 부분을, config 파일을 만들 때 정해 준 Host 명인 git@github.com-git1 으로 변경합니다.$ git clone git@github.com-git1:git1/my.git위와 같이 하면 정상 적으로 다운로드가 완료될 것입니다. 만일, 이미 클론받은 소스가 있다면, 원격 저장소 경로를 아래와 같이 바꿔 줍니다.$ git remode add origin git@github.com-git1:git1/my.git 두번째 계정도 같은 방법으로 클론 또는 원격 저장소를 변경해 줍니다.문제 해결 원격지 퍼미션 오류가 납니다. 이미 세팅이 완료된 상태에서 리붓 후 새로운 계정을 ssh-add 명령으로 등록하면 이전에 설정한 것들이 먹히지 않는 것 같습니다. 모두 ssh-add를 다시 해 주니 정상적으로 되네요. 리붓후에도 등록한 모든 것이 잘 동작합니다. (솔직히 자신 없습니다. 🤣) push할 때 갑자기 아래와 같은 메세지가 나오면서 연결이 되지 않습니다. $ git push ssh: connect to host github.com port 22: Cant assign requested address fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 아래 내용을 .ssh/config 파일안에 넣습니다. 연결할 계정에 대해 Hostname과 port를 변경해 주면 됩니다. Host github.com Hostname ssh.github.com Port 443 참조 SSH (Secure Shell) Home Page 22번 포트로 연결되지 않는 경우" }, { "title": "Jekyll 테마에 utterances 댓글 연동하기", "url": "/posts/utternace-comments-system/", "categories": "jekyll, chirpy", "tags": "jekyll, chirpy, comment, comments, utterances, 댓글, 댓글시스템, git, github, github기반 댓글, disqus", "date": "2021-12-26 14:15:33 +0900", "snippet": " Chirpy는 Disqus를 기본으로 지원합니다. 계정정보만 연동하면 잘 나옵니다.전에 Disqus를 사용했었는데, 무료로 쓰던 기능들이 유료화 하면서 커스터마이징에 어려움이 있습니다. 그리고, UI도 좀 그러네요..그래서 다른 것을 알아보다가 Utterances를 알게 되었습니다.github의 이슈를 댓글로 표시하는 것으로 무료이면서 설치도 단순하고 ui도 깔끔합니다. 관련글 : Jekyll Chirpy 테마 사용하여 블로그 만들기 Chirpy 테마 커스터마이징 작동 원리Utterances가 로드되면 github의 이슈 검색 API를 사용하여 url 또는 Pathname에 기반하여 페이지와 관련된 이슈를 찾습니다. 제목과 일치하는 페이지의 이슈를 찾을 수 없는 경우에도 큰 문제가 없습니다. Utterance-bot이 누군가 처음 댓글을 달 때 자동으로 이슈를 생성합니다.사전 준비우선 github 계정이 필요합니다. Jekyll을 사용하니 당연히 있겠지요? 😅 github 블로그가 아니라면 우선 github 계정을 만들고, 프로젝트를 하나 생성해 둡니다.저는 test project를 사용하겠습니다.저장소는 public으로 만들어야 합니다. 블로그용 저장소가 있을테니, 그 저장소를 그대로 사용해도 됩니다.우선, github에 로그인을 해 둡니다. 다음 단계에서 자동으로 나의 저장소를 Utterances가 찾아 줄 겁니다.내 저장소에 설치하기Utterances 홈페이지의 중간에 보면, utterances app 링크가 있습니다.이 안으로 들어가면 아래 그림처럼 나옵니다.오른쪽 위에 초록색 Install 버튼이 나타납니다.이미 설치 했다면, 회색으로 Configuration 버튼으로 변경되어 보일 겁니다.Install 버튼을 클릭합니다. 그러면 아래 그림처럼 나옵니다.여기에서 Only select repositoryies를 선택합니다.그러면 미리 만들어 둔 나의 저장소가 아래에 뜨게 되고, 이것을 클릭하면 Install 버튼이 활성화 됩니다.설치가 정상적으로 되고 나면 Utterances 홈페이지로 다시 돌아오게 됩니다.Utterances ConfigurationUtterances 홈페이지를 아래로 내려보면, 중간에 Configuration 섹션이 나옵니다. 여기에 나의 저장소를 입력하는 부분이 있습니다. 입력 형식은 [저장소ID]/[저장소명] 입니다. 저는 focuschange-test/focuschange-test.github.io 입력하였습니다.다른 몇몇 설정 값들이 있으나 별로 중요하지 않아서, 변경하지 않고 화면을 내려 보면, Enable Utterances 섹션이 나옵니다. 여기에 자바스크립트 코드가 생성되어 있는데, 이를 복사합니다.자바스크립트 2번째 줄에 repo=...이 설정되어 있는데, 위에서 입력한 저장소명이 그대로 나와 있으면 설정이 끝난 것입니다.Chirpy에 스크립트 심기Jekyll 기반 블로그는 아마도 아래 방법이 비슷할 것입니다._layouts/post.html 파일의 끝에 위에서 복사한 스크립트를 그대로 넣어 줍니다. 여기를 참고하세요~&amp;lt;!-- _layouts/post.html 하단에 삽입 --&amp;gt;... {% endif %} &amp;lt;/div&amp;gt; {% include post-sharing.html %} &amp;lt;/div&amp;gt;&amp;lt;!-- .post-tail-bottom --&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;!-- div.post-tail-wrapper --&amp;gt;&amp;lt;!-- Utterances 댓글 스니핏 삽입 --&amp;gt;&amp;lt;script src=&quot;https://utteranc.es/client.js&quot; repo=&quot;focuschange-test/focuschange-test.github.io&quot; issue-term=&quot;pathname&quot; theme=&quot;github-light&quot; crossorigin=&quot;anonymous&quot; async&amp;gt;&amp;lt;/script&amp;gt;결과 확인자~ 이제 댓글 시스템이 잘 붙었는지 확인해 봅시다. 로컬에서 jekyll serve 명령을 통해 확인해 보겠습니다.아래 처럼 잘 붙어서 나오네요.댓글을 쓰려면 github 로그인을 해야 합니다. Sign In with GitHub 버튼을 클릭해서 로그인 합시다.내용을 입력해 봤습니다. 잘 붙어나오네요. Preview도 지원하고, 마크다운도 쓸 수 있습니다. 아래는 최종 결과입니다.댓글이 어디에 저장되어 있는지 확인해 봅시다. github 저장소에 들어가 보면 상단 탭에 Issues가 있습니다. 이곳을 들어가 보면, 댓글이 달린 포스트 리스트가 보입니다.위에서 작성한 댓글이 posts/welcome/에 들어 있군요. 제 테스트 페이지가 welcome 이었습니다.이것을 클릭해 보면 아래와 같이 나옵니다.저장된 댓글을 여기서 확인할 수 있습니다. 또한 댓글도 달 수 있습니다. 블로그내에서 다는 것이 더 편합니다만, 이곳에서 통합관리할 수 있으니 편하지요.문제 해결 댓글 화면은 나오는데 저장이 되지 않습니다. 저의 경우는, Chirpy 설치 시 fork를 받아서 설치했었는데요. 이런게 하면 Issues 탭이 생기지 않더군요. 아예 zip 파일을 다운 받아서 설치하는 것이 더 편한 것 같습니다. 아니면, clone 받은 후 저장소를 삭제하고 다시 만들어서 소스를 올리세요. 그러면 Issues 탭이 생기면서 정상 동작합니다. 끝맺으며..댓글 다는 시간은 10분 내외였습니다. 그런데 글을 쓰는 것은 하루 종일 걸리네요. 그래도 좋은 댓글 시스템을 찾은 것 같아 좋네요~ 많이 활용하시기 바랍니다~" }, { "title": "jekyll에서 마크다운(Markdown) 사용하기", "url": "/posts/usage-markdown/", "categories": "jekyll, markdown", "tags": "마크다운, markdown, 마크다운-문법, kramdown, chirpy", "date": "2021-12-24 12:15:33 +0900", "snippet": " 한번에 모두 정리하기에는 양이 너무 많고…자주 쓰는데 자꾸 잊어버리는 것만 우선 기록~ 😅기본 사항 일반 텍스트를 마음대로 입력할 수 있다 html tag를 마음대로 입력할 수 있다 처음 접하는 어려움이 줄바꿈이 안된다는 것이다.줄바꿈마크다운 어디서 사용할 수 있는 줄바꿈 형식 : &amp;lt;BR&amp;gt; 입력 테이블내에 줄바꿈 할 때 유용하다 예시 첫번째&amp;lt;br&amp;gt;두번째세번째. 앞줄에 붙어서 나옴 결과 첫번째 두번째. 세번째. 앞줄에 붙어서 나옴 일반 텍스트에서 줄바꿈 형식 : 문장 끝에 공백 2개 이상 입력 예시 첫번째. 여기는 문장 끝에 공백 2개 두번째. 여기는 공백 없음세번째. 앞줄에 붙어서 나옴 결과 첫번째. 여기는 공백 2개 두번째. 여기는 공백 없음 세번째. 앞줄에 붙어서 나옴 블럭 인용구에서 줄바꿈 형식 : 인용구 끝에 \\ 입력. \\뒤에는 아무것도 입력하지 않아야 한다\\ 예시 &amp;gt; 첫번째 줄\\&amp;gt; 두번째 줄\\&amp;gt; 세번째 줄.&amp;gt; 네번째 줄. 앞줄과 붙음 결과 첫번째 줄두번째 줄세번째 줄.네번째 줄. 앞줄과 붙음 블럭 인용구 형식 : &amp;gt;를 앞에 붙인다. 이중 인용구를 사용할 때는 &amp;gt;&amp;gt;를 입력한다 예시 &amp;gt; 첫번째&amp;gt;&amp;gt; 두번째&amp;gt;&amp;gt;&amp;gt; 세번째&amp;gt; 네번째 결과 첫번째 두번째 세번째네번째 코드인라인 코드 형식 : ` 사이에 내용 입력. ` 자체를 인라인 시키고 싶으면 두개를 입력하여 사용하면 escape 됨 예시 인라인 코드는 `하하` 이렇게~ 인라인 코드 자체를 인라인 시키고자 할 때는 `` ` `` 이렇게 입력 결과 인라인 코드는 하하 이렇게~인라인 코드 자체를 인라인 시키고자 할 때는 ` 이렇게 입력 코드 블럭 형식 : &amp;lt;사용할 언어&amp;gt;⏎코드⏎ 사용할 언어는… 음… 목록을 알아서.. ` 대신에 ~를 사용할 수 있다 예시 ```java# 이렇게class a { static string s = &#39;하하&#39;;}``` 결과 # 이렇게class a { static string s = &#39;하하&#39;;} liquid 문법은 코드 블럭에 표시하기소스블럭 내에서 {% raw %} {% endraw %}를 사용한다....{% include lang.html %}{% raw %}{% if page.image.src %} {% capture bg %} {% unless page.image.no_bg %}{{ &#39;bg&#39; }}{% endunless %} {% endcapture %}{% endif %}{% endraw %}...링크 기본 형식 : [링크텍스트](링크) 새창으로 열기 : [링크텍스트](링크){:target=&quot;_blank&quot;} 참고 : {} 사이에 html attribute를 마음대로 넣을 수 있다 예시 - 기본 링크 : [홈으로](https://www.irgroup.org/) - 새창으로 : [홈으로 새창](https://www.irgroup.org/){:target=&quot;_blank&quot;} 결과 기본 링크 : 홈으로 새창으로 : 홈으로 새창 이미지 기본 형식 : ![설명](링크) 테두리 만들기 : ![설명](링크){:style=&quot;border:1px solid #eaeaea; border-radius: 7px; padding: 0px;&quot; } 참고 : {} 사이에 html attribute를 마음대로 넣을 수 있다 예시 ![깃헙 커밋 액션](/assets/img/github-commit-action.jpg){:style=&quot;border:1px solid #eaeaea; border-radius: 7px; padding: 0px;&quot; } 결과참조 kramdown syntax kramdown 사용법 Front Matter" }, { "title": "Chirpy 테마 커스터마이징", "url": "/posts/Chirpy-%ED%85%8C%EB%A7%88-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95/", "categories": "jekyll, chirpy", "tags": "jekyll, chirpy, 커스터마이징, chirpy custormizing", "date": "2021-12-24 12:15:33 +0900", "snippet": " 지난 포스팅에서 Chirpy테마를 사용하여 설치하는 기본적인 절차를 정리하였습니다.이번은 주로 많이 수정하는 부분에 대한 것을 정리해 보겠습니다\\ 관련글 : Jekyll Chirpy 테마 사용하여 블로그 만들기 Jekyll 테마에 utterances 댓글 연동하기 _config.yml 수정블로그 환경 설정을 위한 중요한 설정 파일입나디. 여기를 참고 하시기 바랍니다. local에서 jekyll servce로 구동했을 때, 이 파일이 수정되면 반드시 재구동해 줘야 합니다.언어 설정하기블로그에 기본적으로 정의되어 있는 단어들이 있습니다. 예를 들어 사이드바의 탭메뉴멍 HOME, CATEGORIES 등, 오른쪽의 Recent Update, Trending Tags 등이 이에 해당합니다. 이런 기본적인 단어들을 원하는 언어 설정에 맞게 수정할 수 있습니다. 그러나 언어별로 모든 단어가 준비되어 있지 않습니다. 아래 내용대로 따라하면 모든 명칭을 한글로 바꿀 수 있습니다.언어설정은 _data/locales에 모두 들어 있습니다. 처음 Chirpy를 설치하면 아래와 같이 3개의 파일밖에 없습니다.-rw-r--r-- 1 a2021054 staff 2059 12 20 11:22 en.yml-rw-r--r-- 1 a2021054 staff 2133 12 20 11:22 id-ID.yml-rw-r--r-- 1 a2021054 staff 2015 12 20 11:22 zh-CN.yml기본값은 en.yml입니다. 영어를 말합니다. 만일 없는 언어를 설정한다면 이 파일이 기본으로 적용됩니다.en.yml 파일을 열어보면, 블로그의 각 위치별 명칭이 정의되어 있는 것을 확인할 수 있습니다.en.yml을 복사해서 ko.yml로 만들고 값을 한글로 바꿔 봅시다.저는 아래와 같이 사이브바의 탭메뉴명을 바꿔 보았습니다.# The tabs of sidebartabs: # format: &amp;lt;filename_without_extension&amp;gt;: &amp;lt;value&amp;gt; home: 홈 categories: 카테고리 tags: 태그그리고나서, _config.yml을 열어서 lang: en 으로 되어 있는 부분을 lang: ko로 변경합니다.페이지를 열어보면 아래와 같이 변경된 것을 보실 수 있을 겁니다.(😓 한글 메뉴명이 안 이쁘네요… 다시 영어로.. 🤣)` local에서 jekyll servce로 구동했을 때, 이 파일이 수정되면 반드시 재구동해 줘야 합니다.이미지 업로드 하기외부 url을 입력해도 되지만, 나의 블로그에 이미지를 업로드 하여 설정하는 것도 좋은 방법입니다.이미지가 들어 있는 경로는 assets/img/입니다. 여기에 필요한 이미지를 넣어 두면 됩니다.아바타 바꾸기_config.yml의 avatar 항목에 이미지를 업로드 한 후 경로를 입력합니다.경로는 assets/img/[아바타이미지] 이런 식으로 넣어 주면 됩니다.블로그 타이틀과 서브타이틀 폰트/색상 바꾸기_sass/addon/commons.scss에 타이틀과 서브타이틀에 대한 설정이 있습니다. 아래를 참고해서 바꿉니다.저는 배경이 어두워서 흰색계열로 폰트 색상을 변경하였습니다. (css에 대해 궁금하면 구글링~ 😅) /* 타이틀에 대한 색상 크기 등을 변경할 수 있습니다. */ .site-title { a { @extend %clickable-transition; font-weight: 900; font-size: 1.5rem; letter-spacing: 0.5px; color: rgba(254, 254, 254, 99%); } } /* 서브 타이틀에 대한 색상 크기 등을 변경할 수 있습니다. */ .site-subtitle { font-size: 95%; /* color: var(--sidebar-muted-color); */ color: rgba(254, 254, 254, 99%); line-height: 1.2rem; word-spacing: 1px; margin: 0.5rem 1.5rem 0.5rem 1.5rem; min-height: 3rem; // avoid vertical shifting in multi-line words user-select: none; }왼쪽 사이드바 배경 이미지 넣기_sass/addon/commons.scss에 사이드바 배경에 대한 설정이 있습니다.아래와 같이 변경 합니다.#sidebar { ... /* 원본 내용 background: var(--sidebar-bg); */ /* 아래 3줄 추가 이미지는 assets/img/ 디렉토리에 넣어 주세요. */ background: url(&#39;/assets/img/일출.jpg&#39;); background-size: 100% 100%; background-position: center;사이드바 내 페이스북 아이콘 및 링크 넣기링크대한 정의는 _data/contact.yml에 들어 있습니다. 아래와 같이 내용을 추가합니다. 이곳을 참고하세요- type: facebook icon: &#39;fab fa-facebook&#39; url: &#39;https://www.facebook.com/focuschange&#39;icon은 반드시 위와 같이 넣어주세요. url은 당연히 본인의 폐북 홈 URL로… 사이드바의 하단의 아이콘들은 상당히 깔끔합니다. 헌데, 이것이 오픈소스인가 봅니다. 무료로 아무나 쓸 수 있는 것 같습니다. fontawesome에 가 보시면 이쁜 폰트들을 사용할 수 있습니다. chirpy가 이 폰트들을 기본으로 사용하고 있습니다. Footer 수정 하기_includes/footer.html에 아래 그림처럼 하단의 내용이 정의되어 있습니다.“Powered by …“이 눈에 거슬리는군요. 삭제해 보겠습니다.파일을 열어서 아래 부분을 주석처리 합니다. 삭제해도 무방합니다. 저는 주석처리를 해 두었습니다. 여기를 참고하세요~. &amp;lt;div class=&quot;footer-right&quot;&amp;gt; &amp;lt;p class=&quot;mb-0&quot;&amp;gt; {% capture _platform %} &amp;lt;a href=&quot;https://jekyllrb.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Jekyll&amp;lt;/a&amp;gt; {% endcapture %} {% capture _theme %} &amp;lt;a href=&quot;https://github.com/cotes2020/jekyll-theme-chirpy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Chirpy&amp;lt;/a&amp;gt; {% endcapture %} {{ site.data.locales[lang].meta | default: &#39;Powered by :PLATFORM with :THEME theme.&#39; | replace: &#39;:PLATFORM&#39;, _platform | replace: &#39;:THEME&#39;, _theme }} &amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt;주석은 시작과 끝을 &amp;lt;!--, --&amp;gt;로 막아주면 됩니다.footer의 왼쪽 부분에 Some rights reserved.라고 되어 있네요. 이걸 바꾸려면 _data/locales/en.yml 파일을 엽니다.그 안에, copyright: 아래 brief: 부분을 수정하면 됩니다. 하는 김에, license: 아래 template:을 지워버립시다. 그러면, 포스팅된 글의 하단에 This post is licensed under CC BY 4.0 by the author. 부분이 사라집니다.Utterances 댓글 붙이기Chirpy는 댓글 시스템으로 Disqus를 지원합니다. 한동안 잘 썼었는데, 어느때 부터인가 몇몇 기능들이 유료로 바뀌고, UI가 너무 난잡(?) 합니다. 그래서 Utterances를 적용해 보도록 하겠습니다.자세한 내용은 아래 포스트에서 확인하면 됩니다. Jekyll 테마에 utterances 댓글 연동하기About 편집하기_tabs 디렉토리의 내용을 보면 아래와 같습니다.-rw-r--r-- 1 a2021054 staff 93 12 20 11:23 about.md-rw-r--r-- 1 a2021054 staff 72 12 20 11:23 archives.md-rw-r--r-- 1 a2021054 staff 74 12 20 11:23 categories.md-rw-r--r-- 1 a2021054 staff 59 12 20 11:23 tags.md이 중에 about.md 파일을 수정하면 됩니다. 마크다운 문법은 여기를 참고하세요~ 같은 방법으로, archives.md, categories.md, tags.md 파일들을 수정하면, 왼쪽 탭메뉴에 클릭 시 나오는 페이지를 수정할 수 있습니다.favicon 변경하기브라우저 탭에 나오는 favicon 이미지를 변경하는 방법입니다. favicon에 대해 더 알고 싶으시면 블로그 브랜딩, 파비콘(favicon)만드는 방법 이런 글이 있네요.. Chirpy 테마는 favicon이 기본값 아래 이미지로 되어 있습니다.자신의 이미지로 변경하고 싶으면, 적당한 크기의 이미지를 만든 후, 여기에서 favicons을 만듭니다. 다른 좋은 사이트도 많은 것 같은데.. 그냥 단순해서 여기서 만들어 봤습니다. 생성하게 되면 꽤 많은 파일들이 만들어지게 됩니다.이 중에, favicon으로 시작하는 파일 4개를 assets/img/favicon 디렉토리로 복사해 줍니다. 기존 파일을 엎어 버립시다.post 상단 이미지 style 적용하기_layouts/post.html 파일을 열어보면 아래와 같은 내용이 있습니다.{% if page.image.src %} {% capture bg %} {% unless page.image.no_bg %}{{ &#39;bg&#39; }}{% endunless %} {% endcapture %} &amp;lt;img src=&quot;{{ page.image.src }}&quot; class=&quot;preview-img {{ bg | strip }}&quot; alt=&quot;{{ page.image.alt | default: &#39;Preview Image&#39; }}&quot; {% if page.image.width %} width=&quot;{{ page.image.width }}&quot; {% elsif page.image.w %} width=&quot;{{ page.image.w }}&quot; {% endif %} {% if page.image.height %} height=&quot;{{ page.image.height }}&quot; {% elsif page.image.h %} height=&quot;{{ page.image.h }}&quot; {% endif %}&amp;gt;{% endif %}여기에서 7번째 라인에 원하는 스타일을 적용해 주면 됩니다. 예) 회색으로 이미지 외곽선 추가 : style=”border:1px solid #eaeaea; border-radius: 7px; padding: 0px;”사이트맵 설정sitemap.xml 파일을 생성해서 구글 검색엔진에 노출되는 글을 제어할 수 있습니다.아래를 참고하세요~ Jekyll 테마에서 sitemap.xml 설정하기구글 Analytics 연동하기내 블로그에 얼마나 많은 사람들이 들어왔는지 보려면 구글 애널리틱스를 붙이는 것이 아주 빠른 방법입니다. 물론 사용성도 좋고, 많은 정보를 얻을 수 있습니다. (업무용으로 쓰신다면 비추! 더 좋은 다른 툴이 있습니다.) 아래를 참고하세요~ Jekyll 테마에 구글 애널리틱스 연동하기구글 애드센스 붙이기블로그를 하다보면 광고를 붙이는 경우가 참 많습니다. 저도 한번 붙여 봤는데요. 광고 수익이 올라오는 것을 보면 신기합니다. 이런 글도 읽어 주는 구나.. 😅 헌데, 사이트가 너무 지저분해 집니다. 모바일에서는 탭메뉴 사이에 광고가…. 😡 아무튼, 광고 적용은 아래를 참조 하세요~ Jekyll 테마에 구글 애드센스 붙이기" }, { "title": "Jekyll Chirpy 테마 사용하여 블로그 만들기", "url": "/posts/jekyll-chirpy/", "categories": "jekyll, chirpy", "tags": "jekyll, chirpy, github-blog, github, blog", "date": "2021-12-22 11:17:00 +0900", "snippet": " 오래전 도메인을 살려두기 위해 jekyll로 블로그를 만들어 두었었는데, 항상 UI가 엉성해서 맘에 안들었었습니다.근래에 chirpy 테마를 알게되서 적용해 보니, 예전보다 만드는 방법이 많이 간결해 지고 테마도 상당히 이쁘네요.(나중에 테마 순위 정리나 한번…)기본적인 세팅부터 간단한 커스터마이징까지 정리해 보려합니다.(다른 분들이 너무 잘 정리해 놨지만, 내 환경에 대한 내용을 추가하기 위해 정리합니다)본 글은 github 계정 생성이나 다른 부가적인 내용들은 기록하지 않습니다.오직 chirpy 테마로 블로그 만드는 것에만 집중하여 정리합니다.MacOS 기준으로 작성합니다 관련글 : Chirpy 테마 커스터마이징 Jekyll 테마에 utterances 댓글 연동하기 로컬 환경 설정 하기ruby가 필수이기 때문에 아래 문서를 참고해서 ruby를 설치합니다. 나중에 로컬에서 jekyll을 실행시켜서 환경 설정이나 포스팅 결과를 미리 확인하기 위한 용도입니다. 참고 : https://jekyllrb.com/docs/installation/ brew install ruby Chirpy 테마 설치하기chirpy 테마는 아래 두가지 방법으로 설치할 수 있습니다. chirpy starter를 통해 직접 설치 아주 단순합니다. 처음에 이렇게 쉽게 만들 수 있단 말인가 싶을 정도로 금방 설치가 되었습니다. 그러나, 커스터마이징 할 때 문제가 계속 발생합니다. 물론 모두 해결할 수 있지만 오히려 귀찮습니다. github에서 소스를 fork 받아서 만들기 몇 가지 커스터마이징을 해 주어야 합니다. Jekyll를 조금이라도 사용해 본 적이 있는 분들에게 좀 더 유용할 듯 싶습니다. 그런데, 블로그를 커스터마이징 하겠다면 이 방법을 사용하는 것이 좋습니다. 원하는 대로 쉽게 커스터마이징을 할 수 있습니다. 소스를 zip으로 받아서 설치 fork로 설치하게 되면 나중에 추가 작업이 있을 수 있습니다. 그대로 하면 utterances 댓글 같은 기능들이 오동작할 수도 있습니다. 몇 가지 수정해 주면 되지만, 귀찮습니다. 아예 시작부터 zip을 받아서 하는 것이 더 좋은 것 같습니다. 본 글에서는 2번째 방법을 사용하여 설치하는 법을 정리합니다. ui 커스터마이징이나 google analytics, adsense 등을 붙이기 위해서는 두번째 방법으로 설치하면 훨씬 편하게 작업할 수 있습니다.아래 단계를 따라가면서 설치해 보겠습니다.Chirpy 테마 forkFork Chirpy 를 사용하여 소스를 내가 생성한 저장소로 fork 받습니다.화면에서 나의 계정을 선택하면 바로 fork 되어 아래 화면 처럼 jekyll-theme-chirpy 저장소가 자동 생성됩니다.Repository 이름 바꾸기개인 도메인이 있다면 Repository명을 임의로 줘도 상관 없으나, github의 기본 도메인을 사용할 때는 &amp;lt;github 아이디&amp;gt;.github.io 형식으로 만들어야 합니다.이름을 잘못 정했다고 너무 걱정하지 않아도 됩니다. Repository의 Settings에서 언제든 바꿀 수 있습니다. 저는 개인 도메인이 있으나 테스트로 github 도메인을 사용하기 위해 focuschange-test.github.io로 저장소를 만들겠습니다.나중에 설치가 끝나고 나면 https://focuschange-test.github.io로 블로그에 접속할 수 있을 겁니다. github &amp;gt; fork 받은 저장소 &amp;gt; Settings 화면으로 이동 Repository name을 &amp;lt;github 아이디&amp;gt;.github.io 형식으로 변경 rename 버튼 클릭 저장소 Code 화면으로 돌아오게 되며, 상단에 보면 방금 바뀐 이름으로 저장소 명이 보이게 될 겁니다소스 클론 받기아래 명령을 통해 소스를 받습니다.git clone git@github.com-kim:focuschange-test/focuschange-test.github.io.git git에 대한 기초 지식이 없으신 분은 우선 여기를 참고하세요~저는 멀티계정을 사용하고 있기 때문에 clone url이 좀 다릅니다만, 기본적인 clone 방식으로 받으시면 됩니다.정상적으로 로컬로 소스를 받으셨다면, 기본적인 준비가 끝났습니다.chirpy 초기화 하기소스 홈에서 아래 명령을 사용하여 chirpy를 초기화 합니다.$ tools/init.sh[INFO] Initialization successful! &amp;lt;-- 이런 메세지가 나오면 성공입니다.위의 명령을 수행하면 다음 파일들이 삭제됩니다. .travis.yml _posts 폴더 하위의 파일들 docs 폴더fork 받았기 때문에 chirpy 자체 개발을 위한 파일들을 미리 삭제해 주는 것으로 보입니다.로컬에서 실행해 보기우선 jekyll을 로컬에서 실행시키기 위해 터미널에서 아래 명령을 사용하여 의존성이 있는 모듈을 모두 설치합니다.이미 chirpy에 기본설정이 되어 있기 때문에 아래 명령만으로 필요한 모든 것이 설치 됩니다.$ bundle많은 내용들이 설치되는 것을 볼 수 있습니다.터미널에서 다음 명령을 사용하여 jekyll을 실행시킵니다.$ jekyll serve정상적으로 수행됐다면 아래와 같이 출력됩니다.내용에서 보시면, Server address: http://127.0.0.1:4000/ 라는 것이 있군요.브라우저를 열어서 해당 주소를 입력해 봅시다.그러면 아래와 같이 기본 블로그 화면이 나타납니다.위 화면이 잘 나오면 로컬에서 테스트는 성공입니다.문제 해결위와 같이 진행했을 때 몇가지 문제가 발생할 수 있습니다. 제가 경험했던 것을 기록해 봅니다. command not found: jekyll 로컬 환경에 jekyll이 설치되지 않아서 그렇습니다. 맥OS에 Jekyll 설치 여기를 참조하여 Jekyll을 설치해 줍니다. jekyll serve 명령을 실행하면 오류 메세지가 나옵니다. 여러가지 오류가 발생할 수 있습니다. 우선, Gemfile.lock을 삭제한 후 bundle 명령을 다시 실행해 보세요. 그래도 안된다면.. 구글링 😅 간혹 퍼미션 오류를 내면서 실행이 안되는 경우가 있습니다. 그런 경우는 해당하는 디렉토리와 파일을 모두 권한 변경을 해 주세요. 제 경우는 chmod -R 0644 * 이렇게 처리해 버렸습니다. 😱 (다른 오류를 만들 수 있습니다..) _config.yml을 수정했는데 반영되지 않습니다. jekyll serve로 실행해 둔 것을 ⌃ + C 키를 사용하여 중지 시킨 후 재 시작하면 됩니다 Chirpy 환경 설정소스 둘러보기소스 목록을 보면 아래와 같이 보일 겁니다.사실 모든 것을 알 필요는 없지만, 블로그 환경 설정과 커스터마이징을 위해 몇가지는 알아둘 필요가 있습니다.본 글에서는 환경 세팅만 할 예정이기에 깊게 살펴보지는 않겠습니다만, 몇몇개 내용을 기억해 둡시다. _config.yml : 블로그의 기본 설정 파일입니다. 기본 환경세팅은 모두 여기에서 합니다. _data : 왼쪽 사이드바와 포스트 하단의 공유하기 버튼등의 구성을 변경할 수 있습니다. 언어 설정에 따라 기본적으로 화면에 나오는 단어들을 변경할 수 있습니다. _include : 사이드바, toc, 구글애널리틱스, footer, 댓글 등의 대부분의 모듈형으로 삽입되는 UI를 변경할 수 있습니다 _layout : 블로그 전역에 적용되는 기본 형식, 카테고리, 포스트 등에 적용되는 형식등을 변경할 수 있습니다. _posts : 내가 작성한 블로그 글을 저장해 두는 곳입니다. _sass : css 파일을 커스터마이징 할 수 있습니다. _site : 로컬에서 실행할 때, 화면 UI를 구성하는 모든 내용이 들어 있습니다. 이곳의 내용을 변경하면 로컬에는 잘 반영되지만, git에는 올라가지 않습니다. 또한 다른 기본 디렉토리의 내용을 변경하고 빌드하게 되면 이곳의 내용이 바뀌게 됩니다. _tabs : 왼쪽 사이드바의 기본 탭메뉴들에 대한 랜딩페이지가 들어 있습니다. assets : css, img등이 있습니다. 포스팅에 들어갈 이미지는 assets/img 아래에 넣으면 됩니다. tools : github에서 자동 배포를 위한 코드가 들어 있습니다. 이곳은 아예 건들지 맙시다_config.yml 수정이제 나만의 환경을 위한 블로그 세팅에 들어가 봅시다.수정할 내용은 아래와 같습니다. 항목 값 설명 lang ko 언어를 한글로 설정합니다. 기본값은 en입니다. ko에 대한 언어셋이 없기 때문에 사실상 효과 없습니다. 그러나 나중에 커스터마이징을 위해 해 둡시다. timezone Asia/Seoul 서울 표준시로 설정합니다.(만든분이 중국인인 것 같습니다 😁) title 아무거나~ 블로그 제목을 넣어 줍니다. 아바타 바로 아래 큰 글씨로 표시됩니다 tagline 아무거나~ title 아래에 작은 글씨로 부연설명을 넣을 수 있습니다 description 아무거나~ SEO를 위한 키워드들을 입력합니다.SEO에 대해 골치아프니, 쉽게 생각하자면 구글 검색에 어떤 키워드로 내 블로그를 검색하게 할 것인가를 결정해서 넣으면 됩니다. url https://focuschange-test.github.io 내 블로그로 실제 접속할 url을 입력합니다 github github id 본인의 github 아이디를 입력합니다 twitter.username twitter id 트위터를 사용한다면 아이디를 입력합니다 social.name 이름 포스트 등에 표시할 나의 이름을 입력합니다 social.email 이메일 나의 이메일 계정을 입력합니다 social.links 소셜 링크들 트위터, 페이스등 내가 사용하고 있는 소셜 서비스의 나의 홈 url을 입력합니다 theme_mode light or dark 원하는 테마 스킨을 선택합니다. 기본은 light입니다 avatar 이미지 경로 블로그 왼쪽 상단에 표시될 나의 아바타 이미지를 설정합니다 toc true 포스팅된 글의 오른쪽에 목차를 표시합니다 paginate 10 한 목록에 몇개의 글을 표시해 줄 것인지 선택합니다 제가 설정한 _config.yml을 참조하시면 됩니다. _config.yml을 수정하면 항상 jekyll을 재구동 해 줍니다.첫번째 포스팅 해보기jekyll을 마크다운 문법으로 글을 작성할 수 있습니다. 다양한 툴들이 있으니 이들을 사용하면 되겠네요.마크다운 문법에 대해 간단히 알아보시려면 여기를 확인해 보세요. 편집기는 무엇을 써도 상관 없습니다.jekyll에서 제공하는 jekyll 어드민을 사용하면 초보자들도 쉽게 마크다운 편집을 할 수 있습니다.배포나 관리가 아주 편리한데… chirpy에 설치해 보려니 계속 충돌이 일어납니다. (누가 좀 알려주세요~~ 😭)제가 주로 쓰는 툴은, 웹편집기로 https://stackedit.io/를 사용합니다.로컬에서는 sublime text를 사용합니다.둘 다 장단점이 있긴 하지만 훌륭한 툴들입니다.글을 작성한 후 파일명은 반드시 아래 규칙을 지켜야 합니다. 형식 : yyyy-mm-dd-제목.md 년 4자리, 월 2자리, 일 2자리와 제목을 넣어줍니다. 확장자는 .md 또는 .markdown으로 합니다. .md가 간편하겠지요~ 중간에 공백을 넣지 않습니다. 공백이 필요하면 -로 바꿉니다. 작성한 파일은 _posts 디렉토리에 넣습니다.이제 파일을 만들어 봅시다. 저는 간단히 “안녕하세요~”라고만 넣어 봤습니다. 파일명은 2021-12-23-welcome.md 이렇게 정하고 _posts 디렉토리에 넣었습니다. 브라우저에서 http://127.0.0.1:4000/ 로 접속하면 새로 작성한 글의 목록이 보이게 됩니다.배포소스 올리기로컬에서 모든 테스트가 끝났습니다. 수정한 내용을 github에 올릴 차례입니다. 우선, .gitignore 파일을 열어서 아래 내용을 한줄 추가 합니다.Gemfile.lock이 파일이 git에 올라가면 빌드/배포가 에러를 내는 경우가 많습니다. 그래서 올라가지 않도록 막는 것입니다.아래 git 명령을 사용하여 수정된 내용을 모두 github에 올립니다.(git에 대한 자세한 사용법은 구글링… )$ git add -A...$ git commit -m &quot;first commit&quot; &amp;lt;-- -m 뒤에는 원하는 내용으로~...$ git pushgithub에서 무슨 일이?push를 하게되면 github은 자동으로 블로그 페이지를 만들어 줍니다. 시간이 좀 걸리는데요.. 잠시 살펴 봅시다. github의 저장소에 가보면, Actions라는 탭이 상단에 있습니다. 이를 클릭해 보면 아래와 같이 나타납니다. 위에서 commit할 때, first commit이라고 메세지를 넣어 두었습니다.위 그림에서 보이는 first commit이 그 내용입니다.정상적으로 종료가 된다면 왼쪽에 초록색 체크 아이콘이 붙게 될 겁니다. 위 그림은 진행중이네요. 제 경우 완료하는데 3분 25초가 걸렸습니다.소스를 push하게 되면, github은 자동으로 page build와 deployment를 수행합니다. 이때, 중요한 것이, github이 gh-pages 라는 브랜치를 자동으로 생성해 줍니다.(사실, 이와 관련된 세팅이 이미 chirpy theme에 모두 들어 있습니다)블로그 사이트는 이 브랜치에서 실제 내용이 구성되게 됩니다. 우리가 push한 것은 master 브랜치이고, 여기에는 사이트에 대한 기본 구조가 없습니다. 그래서, 한 가지 더 작업을 해 줘야 합니다.!!!서비스용 브랜치 변경하기저장소의 상단 탭에 Settings가 있습니다. 이곳에서 서비스 페이지의 브랜치를 gh-pages로 변경해 줘야 합니다.아래 그림처럼 순서대로 클릭하면 브랜치 리스트가 보입니다.이중에 gh-pages를 선택하고 Save 버튼을 클릭합니다.브랜치를 바꾸면 또다시 자동으로 build &amp;amp; deployment가 일어납니다. Actions 탭에 가 보면 적용중인 것이 보일 겁니다.블로그 확인하기자동 배포가 완료되면 드디어 블로그 세팅이 끝난 것입니다. 나의 사이트에 들어가서 내용을 확인해 봅시다. 저의 경우는 https://focuschange-test.github.io/가 이렇게 나오네요~성공입니다~뒷정리이제 불필요한 것들을 정리해 봅시다. git branch 삭제 : master, gh-pages 브랜치를 제외하고 모두 삭제합니다. 🤔 더 삭제할 것이… 없군요… 😝마무리하며chirpy는 정말 설치하기도 편하고 디자인도 단순하면서 깔끔합니다. 원했던 검색기능이나 카테고리 기능도 손댈 필요가 없을 정도로 너무 좋네요.front 수정은 상당히 난해(?)해서 최대한 손대지 않는 테마를 찾았었는데, 제게 딱 맞는 것을 찾은 것 같습니다.개발자에게 경의를~ 다음은 간단한 커스터마이징하는 것을 포스팅하겠습니다.참고 깃헙 블로그 만들기 : http://blog.ju-ing.com/posts/Github-blog-chirpy-theme/ chirpy theme github : https://github.com/cotes2020/jekyll-theme-chirpy 마크다운 사용법 총정리 : https://heropy.blog/2017/09/30/markdown/ github 저장소 생성하기 : https://www.lainyzine.com/ko/article/how-to-create-a-new-remote-git-repository-on-github/ git 튜토리얼 : https://evan-moon.github.io/2019/07/25/git-tutorial/" }, { "title": "elasticsearch 6.5에서 payload scoring 구현하기", "url": "/posts/payload-score-in-elasticsearch-6.5/", "categories": "search-engine, elasticsearch", "tags": "elasticsearch,, elasticsearch6.5,, payload,, payload-score,, score,, ranking,, script-score", "date": "2018-11-21 00:00:00 +0900", "snippet": " elastic search가 payload score를 기본으로 지원하지 않는 이유를 잘 모르겠다. 요구하는 사람들이 꽤 있는 것 같은데..아무튼 없으니, 만들어야지…Introductionpayload score란 문서의 키워드에 절대적인 값을 주어 검색을 할 때 이 절대적인 값으로 정렬 등 부가적인 scoring이 가능하도록 구성하는 것을 말한다. 이는 루씬에서 아주 예전부터 지원하던 기능으로 알고 있으며, solr같은 경우는 항상 기본 기능으로 제공한다.헌데, elasticsearch는 아주 예전 버전부터 payload score를 지원하지 않고 있다. 이상한 것은 색인은 할 수 있도록 해 두었다는 것이다. 색인된 payload를 어디서 쓰고 있는지 아직 알 수 없지만, 색인만 하고 검색에 사용할 수 없으니 반쪽짜리 기능이 되어 버렸다. 아니 아예 쓸모없는 기능이 되어 버렸다.대다수의 개발자들이 이런 문제로 인해 플러그인을 개발하여 자체적으로 사용하고 있는 것 같다.하지만, elastic쪽에서 자신들의 전략인지는 모르겠지만 플러그인 개발조차 아주 어렵게 해 둔 것 같다.버전업이 되면서 기존 플러그인에 대한 호환성을 전혀 고려해 주지 않기 때문에, 매 버전마다 새로 개발해야 한다.맘에 들지 않는 정책이지만, 뭐.. 어쩔수 없다. 만들어야지..본 문서는 elasticsearch 6.5.0을 기준으로 작성된 plug-in 개발에 대해 설명한다.여기에 전체 소스가 있으니 참고하기 바란다.index색인 데이터를 구성할 때, 특정 키워드에 가중치를 주기 위해 다음과 같이 field를 구성한다고 가정하자{ &quot;doc_id&quot;: &quot;1&quot; &quot;key&quot;: [ &quot;yellow|3 blue|1.1 red|1&quot; ]}위 예제는 문서 1번에서 key 필드의 키워드들에 대해서 yellow라는 키워드는 가중치를 3으로 주고, blue는 1.1, red는 1로 주기 위해 구성해 본 것이다.우선은 이런 형식의 필드값을 색인하기 위한 tokenizer가 필요하다.elasticsearch는 이런 토큰을 위해 delimited_payload라는 token filter를 제공한다.자세한 사항은 여기를 참조하자.이전 버전에서는 delimited_payload_filter라는 이름으로 사용됐는데, 바뀌었다. (이런.. 버전업 되면 또 어떻게 변할지 모른다.)settings.analysis.analyzer에 tokenizer를 설정해 두면 된다.mappings에서는 해당 필드에 term_vector를 설정해야 한다.payload 값은 term_vector에 들어가게 되는데, term offset 다음에 저장되는 것으로 보여진다.다음과 같이 지정하면 된다. &quot;term_vector&quot;: &quot;with_positions_offsets_payloads&quot;자세한 내용은 여기를 확인하자.헌데, 공식 문서에 with_positions_offsets_payloads 란 것이 없다. (뭐지??)쓰면 된다.다음은 전체적인 settings, mappings 내용이다.elasticsearch setting &amp;amp; mapping{ &quot;settings&quot;: { &quot;index&quot;: { &quot;number_of_shards&quot;: 2, &quot;number_of_replicas&quot;: 0 }, &quot;analysis&quot;: { &quot;analyzer&quot;: { &quot;payload_analyzer&quot;: { &quot;type&quot;: &quot;custom&quot;, &quot;tokenizer&quot;: &quot;payload_tokenizer&quot;, &quot;filter&quot;: [ &quot;payload_filter&quot; ] } }, &quot;tokenizer&quot;: { &quot;payload_tokenizer&quot;: { &quot;type&quot;: &quot;whitespace&quot;, &quot;max_token_length&quot;: 64 } }, &quot;filter&quot;: { &quot;payload_filter&quot;: { &quot;type&quot;: &quot;delimited_payload&quot;, &quot;encoding&quot;: &quot;float&quot; } } } }, &quot;mappings&quot;: { &quot;_doc&quot;: { &quot;properties&quot;: { &quot;key&quot;: { &quot;type&quot;: &quot;text&quot;, &quot;analyzer&quot;: &quot;payload_analyzer&quot;, &quot;term_vector&quot;: &quot;with_positions_offsets_payloads&quot;, &quot;store&quot;: true } } } }}search이제 우리가 필요로 하는 payload score 검색 기능을 구현하기 위해 plug-in을 만들어야 한다.script score 기능으로 plug-in을 만들 것이며, 기본적인 소스는 여기를 참조하면 된다.공식 샘플소스는 gradle로 되어 있는데, 아마도 모두 클로닝해서 컴파일 하면 에러가 날 것이다.java 11을 설치해야 한다.maven dependency는 다음과 같다.&amp;lt;dependencies&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;org.elasticsearch&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;elasticsearch&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;6.5.0&amp;lt;/version&amp;gt; &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt; &amp;lt;/dependency&amp;gt;&amp;lt;/dependencies&amp;gt;6.x버전이 되면서 script plug-in 기본 인터페이스가 ScriptPlugin으로 변경된 것 같다.(사실 이전 버전은 잘 모른다. 5.x버전과 비교해 보니 완전히 바뀌어 있었다)Plug-in Class 생성아래는 plug-in 생성을 위한 코드이다.코드 중에 _SOURCE_VALUE와 _LANG_VALUE를 통해 script score를 명시할 것이다.PayloadScorePlugin.javapublic class PayloadScorePlugin extends Plugin implements ScriptPlugin { @Override public ScriptEngine getScriptEngine(Settings settings, Collection&amp;lt;ScriptContext&amp;lt;?&amp;gt;&amp;gt; contexts) { return new WmpScriptEngine(); } private static class WmpScriptEngine implements ScriptEngine { private final String _SOURCE_VALUE = &quot;payload_score&quot;; private final String _LANG_VALUE = &quot;irgroup&quot;; @Override public String getType() { return _LANG_VALUE; } @Override public &amp;lt;T&amp;gt; T compile(String scriptName, String scriptSource, ScriptContext&amp;lt;T&amp;gt; context, Map&amp;lt;String, String&amp;gt; params) { if (!context.equals(ScoreScript.CONTEXT)) { throw new IllegalArgumentException(getType() + &quot; scripts cannot be used for context [&quot; + context.name + &quot;]&quot;); } // we use the script &quot;source&quot; as the script identifier if (_SOURCE_VALUE.equals(scriptSource)) { ScoreScript.Factory factory = PayloadScoreFactory::new; return context.factoryClazz.cast(factory); } throw new IllegalArgumentException(&quot;Unknown script name &quot; + scriptSource); } @Override public void close() { // optionally close resources } } }PayloadScoreFactory.javapublic final class PayloadScoreFactory implements ScoreScript.LeafFactory { private final Map&amp;lt;String, Object&amp;gt; params; private final SearchLookup lookup; private final String field; private final String term; public PayloadScoreFactory(Map&amp;lt;String, Object&amp;gt; params, SearchLookup lookup) { if (params.containsKey(&quot;field&quot;) == false) { throw new IllegalArgumentException(&quot;Missing parameter [field]&quot;); } if (params.containsKey(&quot;term&quot;) == false) { throw new IllegalArgumentException(&quot;Missing parameter [term]&quot;); } this.params = params; this.lookup = lookup; field = params.get(&quot;field&quot;).toString(); term = params.get(&quot;term&quot;).toString(); } @Override public boolean needs_score() { return false; } @Override public ScoreScript newInstance(LeafReaderContext context) throws IOException { PostingsEnum postings = context.reader().postings(new Term(field, term), PostingsEnum.PAYLOADS); if (postings == null) { return new ScoreScript(params, lookup, context) { @Override public double execute() { return 0.0d; } }; } return new ScoreScript(params, lookup, context) { int currentDocid = -1; @Override public void setDocument(int docid) { /* * advance has undefined behavior calling with * a docid &amp;lt;= its current docid */ if (postings.docID() &amp;lt; docid) { try { postings.advance(docid); } catch (IOException e) { throw new UncheckedIOException(e); } } currentDocid = docid; } @Override public double execute() { if (postings.docID() != currentDocid) { /* * advance moved past the current doc, so this doc * has no occurrences of the term */ return 0.0d; } try { int freq = postings.freq(); float sum_payload = 0.0f; for(int i = 0; i &amp;lt; freq; i ++) { postings.nextPosition(); BytesRef payload = postings.getPayload(); if(payload != null) { sum_payload += ByteBuffer.wrap(payload.bytes, payload.offset, payload.length) .order(ByteOrder.BIG_ENDIAN).getFloat(); } } return sum_payload; } catch (IOException e) { throw new UncheckedIOException(e); } } }; } }PayloadScorePlugin class는 script score 기능을 추가하기 위한 plugin 설정 정보를 담는다.실제 score는 PayloadScoreFactory class에서 계산하게 된다.index field에는 동일한 term이 여러개 있을 수 있으며, 각 term마다 payload값을 다르게 줄 수 있다.따라서 이를 모두 합산하여 score를 주기 위해 posting을 순회하면서 합산한다.소스는 짧고 간단하니, 상세한 설명은.. 패스~컴파일 및 설치컴파일은 다음과 같이 하면 된다.maven clean packagezip파일이 잘 만들어졌다면 다음과 같이 설치한다.# 이전 설치된 플러그인 삭제$ ES_HOME/bin/elasticsearch-plugin remove payload_score# 플러그인 등록$ ES_HOME/bin/elasticsearch-plugin install file:///PROJECT_HOME/releases/payload_score-1.0.0.zip물론 elasticsearch를 중단한 후 재구동해야 한다.재구동 할 때, 다음 메세지가 출력되면 성공적으로 등록된 것이다.[2018-11-21T22:04:11,793][INFO ][o.e.p.PluginsService ] [standalone] loaded plugin [payload_score]example test테스트를 위해 아래와 같이 간단히 bulk index용 컬렉션을 만들었다.test_collection.json{ &quot;index&quot; : { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;1&quot; } } { &quot;key&quot; : [&quot;yellow|3 blue|1.1 red|1&quot; ] } { &quot;index&quot; : { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;2&quot; } } { &quot;key&quot; : [&quot;yellow|2 yellow|2.5 blue|1 red|5&quot;] } { &quot;index&quot; : { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;3&quot; } } { &quot;key&quot; : [&quot;yellow|10 blue|2 red|4.3&quot; ] } { &quot;index&quot; : { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;4&quot; } } { &quot;key&quot; : [&quot;dark_yellow|10 blue|3 red|4.2&quot; ] } { &quot;index&quot; : { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;5&quot; } } { &quot;key&quot; : [&quot;yellow|102020.95 blue red|1&quot; ] }색인 생성 및 벌크로드를 아래 스크립트로 간단히 생성하였으니, 유용하게.. (쿨럭~)create_index.sh#!/bin/bash OPTIND=1 # Reset in case getopts has been used previously in the shell. INDEX_NAME=&#39;payload_test&#39; ES_HOST=&#39;localhost:9200&#39; SET_MAP=&#39;set-map.json&#39; function help { echo &quot;Usage : $0 [options]&quot; echo &quot; -h|-? help&quot; echo &quot; -H {host:port} host:port. \\&quot;localhost:9200\\&quot; is default &quot; echo &quot; -i {index} index&quot; echo &quot; -s {set-map_file} setting &amp;amp; mapping json file&quot; echo &quot;&quot; } if [ &quot;$#&quot; -eq 0 ]; then help exit; fi while getopts &quot;H:i:s:&quot; opt; do case &quot;$opt&quot; in \\h|\\?) help exit 0 ;; H) ES_HOST=$OPTARG ;; i) INDEX_NAME=$OPTARG ;; s) SET_MAP=$OPTARG ;; :) echo &quot;Option -$OPTARG requires an argument.&quot; &amp;gt;&amp;amp;2 echo &quot;&quot; help exit 0 ;; esacdone shift $((OPTIND-1)) [ &quot;$1&quot; = &quot;--&quot; ] &amp;amp;&amp;amp; shift echo &#39;* remove old index&#39; curl -X DELETE http://${ES_HOST}/${INDEX_NAME}?pretty echo echo echo &#39;* create index&#39; curl -H &#39;Content-Type: application/json&#39; -XPUT http://${ES_HOST}/${INDEX_NAME}?pretty --data-binary @${SET_MAP} echo echo echo &#39;* index info&#39; curl -XGET http://${ES_HOST}/${INDEX_NAME}?pretty echo echo &#39;* bulk insert&#39; curl -H &#39;Content-Type: application/json&#39; -X POST http://${ES_HOST}/_bulk?pretty --data-binary @test_collection.json echo아래와 같이 사용하면 된다../create_index.sh -i payload-test -s payload_set-map.json정상적이라면 localhost:9200 클러스터에 payload-test index가 생성되어 있을 것이다.다음과 같이 test_collection을 색인해 보자curl -H &#39;Content-Type: application/json&#39; -X POST http://localhost:9200/_bulk?pretty --data-binary @test_collection.json색인된 payload 값을 확인해 보자curl -H &#39;Content-Type: application/json&#39; -XGET localhost:9200/payload-test/_doc/1/_termvectors?pretty아마도 아래처럼 나오게 될 것이다.{ &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;1&quot;, &quot;_version&quot; : 2, &quot;found&quot; : true, &quot;took&quot; : 0, &quot;term_vectors&quot; : { &quot;key&quot; : { &quot;field_statistics&quot; : { &quot;sum_doc_freq&quot; : 12, &quot;doc_count&quot; : 4, &quot;sum_ttf&quot; : 13 }, &quot;terms&quot; : { &quot;blue&quot; : { &quot;term_freq&quot; : 1, &quot;tokens&quot; : [ { &quot;position&quot; : 1, &quot;start_offset&quot; : 9, &quot;end_offset&quot; : 17, &quot;payload&quot; : &quot;P4zMzQ==&quot; } ] }, &quot;red&quot; : { &quot;term_freq&quot; : 1, &quot;tokens&quot; : [ { &quot;position&quot; : 2, &quot;start_offset&quot; : 18, &quot;end_offset&quot; : 23, &quot;payload&quot; : &quot;P4AAAA==&quot; } ] }, &quot;yellow&quot; : { &quot;term_freq&quot; : 1, &quot;tokens&quot; : [ { &quot;position&quot; : 0, &quot;start_offset&quot; : 0, &quot;end_offset&quot; : 8, &quot;payload&quot; : &quot;QEAAAA==&quot; } ] } } } }}tokens에 보면 payload 값이 보인다.문서는 float으로 구성했지만, 엔진에서 base64로 인코딩한다.자.. 이제 대망의 payload score 검색을 해 보자$ curl -H &#39;Content-Type: application/json&#39; -X POST &#39;localhost:9200/payload-test/_search?pretty&#39; -d &#39;{ &quot;query&quot;: { &quot;function_score&quot;: { &quot;query&quot;: { &quot;match&quot;: { &quot;key&quot;: &quot;yellow&quot; } }, &quot;functions&quot;: [ { &quot;script_score&quot;: { &quot;script&quot;: { &quot;source&quot;: &quot;payload_score&quot;, &quot;lang&quot; : &quot;irgroup&quot;, &quot;params&quot;: { &quot;field&quot;: &quot;key&quot;, &quot;term&quot;: &quot;yellow&quot; } } } } ] } }}&#39;fuction_score query를 사용하여 검색한다. script_score에서 source, lang을 프로그램에서 설정한 값으로 입력하고, params 부분에서는 검색대상 필드와 검색할 키워드를 입력한다.결과는 다음과 같이 나올 것이다.{ &quot;took&quot; : 73, &quot;timed_out&quot; : false, &quot;_shards&quot; : { &quot;total&quot; : 2, &quot;successful&quot; : 2, &quot;skipped&quot; : 0, &quot;failed&quot; : 0 }, &quot;hits&quot; : { &quot;total&quot; : 4, &quot;max_score&quot; : 102020.95, &quot;hits&quot; : [ { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;5&quot;, &quot;_score&quot; : 102020.95, &quot;_source&quot; : { &quot;key&quot; : [ &quot;yellow|102020.95 blue red|1&quot; ] } }, { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;3&quot;, &quot;_score&quot; : 10.0, &quot;_source&quot; : { &quot;key&quot; : [ &quot;yellow|10 blue|2 red|4.3&quot; ] } }, { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;2&quot;, &quot;_score&quot; : 4.5, &quot;_source&quot; : { &quot;key&quot; : [ &quot;yellow|2 yellow|2.5 blue|1 red|5&quot; ] } }, { &quot;_index&quot; : &quot;payload-test&quot;, &quot;_type&quot; : &quot;_doc&quot;, &quot;_id&quot; : &quot;1&quot;, &quot;_score&quot; : 3.0, &quot;_source&quot; : { &quot;key&quot; : [ &quot;yellow|3 blue|1.1 red|1&quot; ] } } ] }}잘 나온다. 성공이다~내용을 좀 더 훓어보면, yellow라는 키워드로 검색할 때, score 값이 payload 값으로 나오는 것을 확인 할 수 있다.이때, _id = 2인 문서는 yellow 키워드가 2개이고, 각각의 payload 값을 합산한 것이 score에 반영된 것을 알 수 있다.참조 https://www.elastic.co/guide/en/elasticsearch/plugins/current/plugin-authors.html https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-engine.html https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-delimited-payload-tokenfilter.html https://github.com/elastic/elasticsearch/tree/master/plugins/examples/script-expert-scoring https://github.com/focuschange/elasticsearch-payload-score" }, { "title": "MySQL Login path 설정하기", "url": "/posts/mysql-login-path/", "categories": "tools, mysql", "tags": "mysql,, login,, path", "date": "2018-11-13 00:00:00 +0900", "snippet": " mysql 5.6부터 보안 문제로 인해 패스워드를 커맨드라인에서 직접 입력하기 어려워졌습니다.대신에 login path라는 것을 사용하면 되는데요. 커맨드를 자주 잊어버리네요. “Warning: using a password on the command line interface can be insecure. “ 이런 메세지가 출력될 때 사용하면 되겠네요.간단한 사용법을 정리해 봅니다.login path help $ mysql_config_editor --help mysql_config_editor Ver 1.0 Distrib 5.7.18, for linux-glibc2.5 on x86_64 Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. MySQL Configuration Utility. Usage: mysql_config_editor [program options] [command [command options]] -#, --debug[=#] This is a non-debug version. Catch this and exit. -?, --help Display this help and exit. -v, --verbose Write more information. -V, --version Output version information and exit. Variables (--variable-name=value) and boolean options {FALSE|TRUE} Value (after reading options) --------------------------------- ---------------------------------------- verbose FALSE Where command can be any one of the following : set [command options] Sets user name/password/host name/socket/port for a given login path (section). remove [command options] Remove a login path from the login file. print [command options] Print all the options for a specified login path. reset [command options] Deletes the contents of the login file. help Display this usage/help information.흠.. 간단하네요..generate login path$ mysql_config_editor set --login-path=설정이름 --host=주소 --user=아이디 --port=포트 --passwordEnter password: *****print login path$ mysql_config_editor print --login-path=설정이름[myroot]user = rootpassword = *****host = localhostport = 3306설정된 전체 목록을 출력하려면 다음과 같이 합니다.mysql_config_editor print --allremove login pathmysql_config_editor remove --login-path=설정이름use login path$ mysql --login-path=설정이름흠.. 편하네요. 스크립트로 자동화할 때 훨씬 편하게 세팅할 수 있습니다.location of config file윈도우인 경우는 %APPDATA%\\MySQL 디렉토리에 .mylogin.cnf 파일이 생성됩니다. unix계열은 로그인 계정의 홈 디렉토리에 파일이 생성됩니다. 이 파일은 암호화 되어 있는 것으로 보이네요. 당연하겠지요Troubleshooting “ERROR 1045 (28000): Access denied for user” 에러가 발생하는 경우 당황하지 말고, 패스워드 입력할 때, &quot;를 앞뒤로 붙여서 입력합니다. 특수기호(#,$,! 등)이 들어가 있으면 발생할 수 있습니다. 참조 https://dev.mysql.com/doc/refman/8.0/en/mysql-config-editor.html" }, { "title": "FCM push notification을 위한 앱서버 구현하기 1 - 기본개념 및 core", "url": "/posts/fcm-app-server-1/", "categories": "java, app engine, gcp, fcm, push", "tags": "app-engine, fcm, push, push-notification, gcp, java", "date": "2018-08-03 12:15:33 +0900", "snippet": " 본 문서는 java로 FCM app server를 구축하기 위한 방법을 설명합니다.push만 대상으로 하기 때문에 굳이 XMPP를 사용할 필요가 없어서 HTTP 방식을 사용할 예정이고, 최신 api spec인 - FCM HTTP v1 API을 사용합니다.필요한 서버 환경 클라이언트와 통신할 수 있습니다. FCM 서버에 올바르게 형식이 지정된 메시지 요청을 보낼 수 있습니다. 지수 백오프를 사용하여 요청을 처리하고 다시 보낼 수 있습니다. 서버 키와 클라이언트 등록 토큰을 안전하게 저장할 수 있습니다. 참고: 클라이언트 코드에 절대로 서버 키를 포함해서는 안 됩니다. XMPP의 경우 전송하는 각 메시지를 고유하게 구별하기 위해 서버에서 메시지 ID를 생성할 수 있어야 합니다. FCM HTTP 연결 서버는 메시지 ID를 생성하여 응답에서 반환합니다. XMPP 메시지 ID는 발신자 ID별로 고유해야 합니다.FCM 서버와 상호작용 방법 Admin SDK 원시 프로토콜 FCM HTTP v1 API는 가장 최신 프로토콜로서 보다 안전한 인증과 유연한 교차 플랫폼 메시징 기능을 제공 이전의 HTTP 및 XMPP 서버 프로토콜 영역도 사용 가능 클라이언트 애플리케이션에서 업스트림 메시징을 사용하려면 XMPP를 사용해야 함node.js로 앱서버 구축시는 Admin Node.js SDK의 FCM API를 사용하여 메시지를 보내야 함 XMPP 메시징은 다음 방식에 있어서 HTTP 메시징과 다릅니다. 업스트림/다운스트림 메시지 HTTP: 클라우드에서 기기로 전송하는 다운스트림 전용 XMPP: 기기에서 클라우드로 전송하는 업스트림 및 클라우드에서 기기로 전송하는 다운스트림 메시징(동기 또는 비동기) HTTP: 동기 메시징입니다. 앱 서버가 HTTP POST 요청으로 메시지를 보내고 응답을 기다립니다. 이 방식은 동기 메시징이며 응답이 수신될 때까지 발신자가 다른 메시지를 보내지 못하도록 차단합니다. XMPP: 비동기 메시징입니다. 앱 서버가 영구 XMPP 연결을 통해 최대 회선 속도로 모든 기기에서 양방향으로 메시지를 주고받습니다. XMPP 연결 서버가 확인 또는 실패 알림을 비동기 방식으로 보냅니다. 확인 형식은 JSON으로 인코딩된 특수한 ACK 및 NACK XMPP 메시지입니다. JSON HTTP: JSON 메시지는 HTTP POST로 전송됩니다. XMPP: JSON 메시지는 XMPP 메시지에 캡슐화됩니다. 일반 텍스트 HTTP: 일반 텍스트 메시지는 HTTP POST로 전송됩니다. XMPP: 지원되지 않습니다. 여러 등록 토큰에 전송되는 멀티캐스트 다운스트림 HTTP: JSON 메시지 형식으로 지원됩니다. XMPP: 지원되지 않습니다. HTTP 서버 프로토콜 구현메시지를 보내려면 앱 서버에서 JSON 키-값 쌍으로 구성된 HTTP 헤더와 HTTP 본문을 포함하는 POST 요청을 만듭니다. 헤더 및 본문의 옵션에 관한 자세한 내용은 앱 서버 보내기 요청 작성을 참조HTTP v1의 장점 액세스 토큰을 통한 보안 향상: HTTP v1 API는 OAuth2 보안 모델에 따라 수명이 짧은 액세스 토큰을 사용합니다. 액세스 토큰이 공개되는 경우에도 만료되기 전에 1시간 정도만 악의적으로 사용될 수 있습니다. 새로고침 토큰이 이전 API에서 사용하는 보안 키만큼 자주 전송되지 않으므로 캡처될 가능성이 매우 낮습니다. 여러 플랫폼에서 보다 효율적인 메시지 맞춤설정: 메시지 본문의 경우 HTTP v1 API에 모든 대상 인스턴스에 전달되는 공용 키는 물론 여러 플랫폼의 메시지를 맞춤설정할 수 있는 플랫폼별 키가 있습니다. 이러한 키를 사용하면 메시지 하나로 여러 클라이언트 플랫폼에 약간 다른 페이로드를 전송하는 ‘재정의’를 만들 수 있습니다. 새 클라이언트 플랫폼 버전을 위한 확장성 강화 및 미래 경쟁력 확보: HTTP v1 API는 iOS, Android, 웹에 제공되는 메시지 옵션을 완전히 지원합니다. 각 플랫폼마다 JSON 페이로드에 자체 정의된 블록이 있으므로 FCM에서 필요에 따라 새 버전과 새 플랫폼으로 API를 확장할 수 있습니다. HTTP v1 단점 기기 그룹 메시징이나 멀티캐스트 메시징을 사용하는 앱은 다음 버전의 API를 기다리는 것이 좋습니다. HTTP v1은 이전 API의 해당 기능을 지원하지 않습니다.이제부터는 기존 프로토콜에 대한 언급은 피하겠습니다.필요하다면 https://firebase.google.com/docs/cloud-messaging/server?hl=ko 참고하시길~보내기 요청 승인서비스 계정을 인증하고 Firebase 서비스에 액세스하도록 승인하려면 JSON 형식의 비공개 키 파일을 생성하고 이 키를 사용하여 수명이 짧은 OAuth 2.0 토큰을 발급받아야 합니다. 유효한 토큰을 확보했으면 원격 구성, FCM 등 다양한 Firebase 서비스의 요구사항에 따라 서버 요청에 토큰을 추가할 수 있습니다.다른 서비스 계정을 사용하는 경우 편집자 또는 소유자 권한이 있어야 합니다.서비스 계정에 대한 비공개 키 파일을 생성하는 방법 Firebase 콘솔에서 설정&amp;gt; 서비스 계정을 열기. 새 비공개 키 생성을 클릭하고 키 생성을 클릭하여 확인. 키가 들어있는 JSON 파일을 안전하게 저장.엑세스 토큰을 발급받기 위해서 비공개 키 Json은 필수입니다.액세스 토큰을 발급받는 방법토큰을 발급받으려면 사용할 언어에 대한 Google API 클라이언트 라이브러리를 사용하여 다음과 같이 비공개 키 JSON 파일을 참조합니다.private static String getAccessToken() throws IOException { GoogleCredential googleCredential = GoogleCredential .fromStream(new FileInputStream(&quot;service-account.json&quot;)) .createScoped(Arrays.asList(SCOPES)); googleCredential.refreshToken(); return googleCredential.getAccessToken(); }service--account.json에 Firebase console에서 받은 json 파일을 입력합니다. 꽤나 헤매던 부분인데, SCOPES에 대한 언급을 찾을 수 없었습니다.답은 아래 url을 참고하시면 됩니다.https://stackoverflow.com/questions/47325150/where-to-get-scopes-dependencies-for-java-for-new-firebase-cloud-message-api저는 아래 3개 url로 세팅했습니다. https://www.googleapis.com/auth/firebase https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/firebase.readonlyAccessToken은 보안을 위해 주기적으로 바뀌는데, 위 소스를 적용하면 토큰이 만료되면 토큰 새로고침 메소드가 자동으로 호출되어 업데이트된 토큰이 발급됩니다.HTTP 요청 헤더에 액세스 토큰을 추가하는 방법Authorization 헤더의 값으로 토큰을 Authorization: Bearer &amp;lt;access_token&amp;gt; 형식으로 추가합니다.URL url = new URL(FCM_SEND_ENDPOINT); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestProperty(&quot;Authorization&quot;, &quot;Bearer &quot; + getAccessToken()); httpURLConnection.setRequestProperty(&quot;Content-Type&quot;, &quot;application/json; UTF-8&quot;); return httpURLConnection;여기까지 잘 됐다면 보낼 준비가 끝난 상태입니다. 아직 프로그램을 구동시켜보지 못하겠지만, 다음 단계에서 curl을 이용하여 아래와 같이 테스트 해 볼 수 있습니다.앱 서버 보내기 요청 작성보내기 요청의 전송 유형 주제 이름 조건 기기 등록 토큰 기기 그룹 이름(기존 프로토콜만 해당)자세한 내용은 메시지 유형을 참조 바랍니다.send endpoint는 다음과 같이 구성됩니다.POST https://fcm.googleapis.com/v1/projects/[myproject-name]/messages:send자신의 endpoint url은 Firevase 콘솔의 일반 프로젝트 설정 탭에서 확인할 수 있습니다. 단지, 프로젝트 ID만 알고 있으면 됩니다.특정 기기에 메시지 전송메세지 전송은 https POST로 전송합니다.curl -X POST -H &quot;Authorization: Bearer ya29.c.El7uBYyvqs1...&quot; -H &quot;Content-Type: application/json&quot; -d &#39;{&quot;message&quot;:{ &quot;notification&quot;: { &quot;title&quot;: &quot;FCM Message&quot;, &quot;body&quot;: &quot;This is an FCM Message&quot;, }, &quot;token&quot;: &quot;fh1Ego6mhk0:APA91bF...&quot; }}&#39; &quot;https://fcm.googleapis.com/v1/projects/my-fcm-project/messages:send&quot;Authorization에 access token을 넣어줍니다. message.token 은 클라이언트 사용자의 등록 토큰입니다.만일 클라이언트가 준비되어 있다면 거기서 생성된 등록 토큰을 넣어주면 됩니다.정상적으로 전송되면 아래와 같은 응답이 오게 됩니다.{ &quot;name&quot;: &quot;projects/myproject-b5ae1/messages/0:1500415314455276%31bd1c9631bd1c96&quot; }요청 본문에 대해 보다 자세한 사항은 FCM 메시지 정보 또는 API 참조를 확인하세요코드 작성FCM Server Key를 받았다면 Access Token을 다음과 같이 받아올 수 있습니다.헌데, 이 작업은 구글 API 클라이언트 라이브러리를 사용합니다.따라서 maven 의존성을 아래와 같이 설정해 줍니다.&amp;lt;project&amp;gt; &amp;lt;dependencies&amp;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;/dependencies&amp;gt; &amp;lt;/project&amp;gt;access token 을 받아오기 위한 클래스를 아래와 같이 정의합니다.package com.mysite.fcm.manager; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import java.io.IOException; import java.io.InputStream; import java.util.List; public class AccessToken { public static String getAccessToken(String keyFile, List&amp;lt;String&amp;gt; scopes) throws IOException { InputStream resourceAsStream = AccessToken.class .getResourceAsStream(&quot;/&quot; + keyFile); GoogleCredential googleCredential = GoogleCredential .fromStream(resourceAsStream) .createScoped(scopes); googleCredential.refreshToken(); return googleCredential.getAccessToken(); } } Firebase 예제들은 dependency, import를 명시적으로 알려주지 않더군요.이런거 찾다가 시간 다 보낸다는…. :sob:메세지를 보내기 위한 클래스를 작성합니다.지저분한 코드는 나중에 정리.. 안되어 있으면 알아서 보시길~package com.mysite.fcm.manager; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; public class Push { String endpoint; List&amp;lt;String&amp;gt; scope; String keyFile; String accessToken; HttpURLConnection http; StringBuffer responseBody; public Push() throws IOException { this.endpoint = System.getProperty(&quot;endpoint&quot;); this.keyFile = System.getProperty(&quot;fcm_key&quot;); String tmp[] = System.getProperty(&quot;scope&quot;).split(&quot;,&quot;); this.scope = new ArrayList&amp;lt;&amp;gt;(); for (String s : tmp) { this.scope.add(s); } this.accessToken = AccessToken.getAccessToken(keyFile, scope); responseBody = new StringBuffer(); } public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } public String getAccessToken() { return accessToken; } public void setAccessToken(String accessToken) { this.accessToken = accessToken; } public StringBuffer getResponseBody() { return responseBody; } public String send(String userToken, String message) throws IOException { URL url = new URL(endpoint); http = (HttpURLConnection) url.openConnection(); http.setRequestMethod(&quot;POST&quot;); http.setDoInput(true); http.setRequestProperty(&quot;Authorization&quot;, &quot;Bearer &quot; + accessToken); http.setRequestProperty(&quot;Content-Type&quot;, &quot;application/json; UTF-8&quot;); http.setDoOutput(true); OutputStream os = http.getOutputStream(); String body = &quot;{\\n&quot; + &quot;\\&quot;message\\&quot;:{\\n&quot; + &quot; \\&quot;notification\\&quot;: {\\n&quot; + &quot; \\&quot;title\\&quot;: \\&quot;FCM Message\\&quot;,\\n&quot; + &quot; \\&quot;body\\&quot;: \\&quot;&quot; + message + &quot;\\&quot;,\\n&quot; + &quot; },\\n&quot; + &quot; \\&quot;token\\&quot;: \\&quot;&quot; + userToken + &quot;\\&quot;\\n&quot; + &quot; }\\n&quot; + &quot;}\\n&quot;; System.out.println(body); os.write(body.getBytes()); os.flush(); os.close(); System.out.println(&quot;* CODE : &quot; + http.getResponseCode()); System.out.println(&quot;* MSG : &quot; + http.getResponseMessage()); if(http.getResponseCode() == 200) { BufferedReader br = new BufferedReader(new InputStreamReader(http.getInputStream(), &quot;UTF8&quot;)); String line; while ((line = br.readLine()) != null) { responseBody.append(line); } br.close(); } http.disconnect(); return http.getResponseMessage(); } }이제 push main만 만들면 된다.public static void main(String[] args) throws IOException { Push push = new Push(); try { push.send(&quot;fPmC2-5G8dU:APA...(client 등록 토큰 입력)&quot;, &quot;잘 갑니다~~&quot;); } catch (IOException e) { e.printStackTrace(); } }실행해 보니 잘 가네요.문제 해결처음 보낼 때, 403 에러가 뜨면서 아래와 같이 리턴되는 경우가 있다.{ &quot;error&quot;: { &quot;code&quot;: 403, &quot;message&quot;: &quot;Firebase Cloud Messaging API has not been used in project 42759??????? before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/fcm.googleapis.com/overview?project=42759??????? then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.&quot;, &quot;status&quot;: &quot;PERMISSION_DENIED&quot;, &quot;details&quot;: [ { &quot;@type&quot;: &quot;type.googleapis.com/google.rpc.Help&quot;, &quot;links&quot;: [ { &quot;description&quot;: &quot;Google developers console API activation&quot;, &quot;url&quot;: &quot;https://console.developers.google.com/apis/api/fcm.googleapis.com/overview?project=42759???????&quot; } ] } ] }}아마도 FCM이 구글과 통합되면서 Google Api 인증을 받아야만 하는 것으로 보인다.Google API에서는 각 api별로 사용유무를 설정할 수 있는데, FCM은 기본값이 사용하지 않도록 되어 있어서 그런 것으로 보인다. 해당 링크로 들어가서 사용 버튼을 클릭해 주면 몇분 이내로 에러없이 보낼 수 있다. 필자는 계정을 여러개 쓰고 있는데, 해당 프로젝트를 못찾아서 한참을 헤메고 있었다. Firebase 계정을 로그인 된 상태에서 해당 Url을 클릭하면 프로젝트 설정화면이 나타난다.이런 뻘짓좀 않했으면.. :sob:참고https://firebase.google.com/docs/cloud-messaging/?hl=ko" }, { "title": "자바 어플리케이션을 gcp app engine으로 구성하기 2 - CloudSQL 연동", "url": "/posts/java-app-engine-in-gcp-2/", "categories": "java, app engine, gcp, CloudSQL", "tags": "app-engine, cloudSql, gcp, java", "date": "2018-07-24 00:00:00 +0900", "snippet": "관련 포스팅 IntelliJ에 Google Cloud Tools 연동하기 자바 어플리케이션을 gcp app engine으로 구성하기 1 - 예제 실행해 보기 자바 어플리케이션을 gcp app engine으로 구성하기 2 - CloudSQL 연동 - 이 문서목표 앱엔진에서 CloudSQL 연동하기Cloud SQL instance 세팅 Cloud SQL 인스턴스는 Second Generation으로 만들자 Create a Second Generation Cloud SQL instance 이 포스팅은 sql instance가 이미 생성되어 있고, 데이터베이스, 테이블, 사용자 등이 모두 생성되어 있다는 가정하에 작성됐다. Cloud SDK를 사용하여 connection name 가져오기 아래 명령을 사용하면 instance1에 대한 상세한 정보가 출력된다 $ gcloud sql instances describe instance1 출력되는 내용중에 connectionName 필드에 대한 값을 가져오면 된다. 형태는 project:zone:instance 형식이다. Connection Url Java7과 Java8의 connection url 구성이 다르다. 구글에서 java7 버전이 deprecate 됐기 때문에 이젠 java8로만 작성하자. java 8 Conntection URL 구성 jdbc:mysql://google/&amp;lt;DATABASE_NAME&amp;gt;?cloudSqlInstance=&amp;lt;INSTANCE_CONNECTION_NAME&amp;gt;&amp;amp;socketFactory=com.google.cloud.sql.mysql.SocketFactory&amp;amp;user=&amp;lt;MYSQL_USER_NAME&amp;gt;&amp;amp;password=&amp;lt;MYSQL_USER_PASSWORD&amp;gt;&amp;amp;useSSL=false Java 7 Connection URL 구성(deprecated) Deploy : jdbc:google:mysql://&amp;lt;INSTANCE_CONNECTION_NAME&amp;gt;/&amp;lt;DATABASE_NAME&amp;gt;?user=&amp;lt;MYSQL_USER_NAME&amp;gt;&amp;amp;password=&amp;lt;MYSQL_USER_PASSWORD&amp;gt; Local Testing : jdbc:mysql://google/&amp;lt;DATABASE_NAME&amp;gt;?cloudSqlInstance=&amp;lt;INSTANCE_CONNECTION_NAME&amp;gt;&amp;amp;socketFactory=com.google.cloud.sql.mysql.SocketFactory&amp;amp;user=&amp;lt;MYSQL_USER_NAME&amp;gt;&amp;amp;password=&amp;lt;MYSQL_USER_PASSWORD&amp;gt;&amp;amp;useSSL=false 이하 Java7에 대한 설명은 배제한다 POM에 환경변수 설정하기 환경변수 설정은 gradle 프로젝트로 만들어도 가능할 것으로 보인다. 그러나 Maven으로 하는 것이 정신건강에 이로울 것이니, Cloud SQL연동하려면 무조건 Maven으로 하자. 아래는 pom.xml의 일부 예시이다. &amp;lt;properties&amp;gt; &amp;lt;!-- INSTANCE_CONNECTION_NAME from Cloud Console &amp;gt; SQL &amp;gt; Instance Details &amp;gt; Properties or gcloud sql instances describe &amp;lt;instance&amp;gt; project:region:instance for Cloud SQL 2nd Generation or project:instance for Cloud SQL 1st Generation --&amp;gt; &amp;lt;INSTANCE_CONNECTION_NAME&amp;gt;project:region:instance&amp;lt;/INSTANCE_CONNECTION_NAME&amp;gt; &amp;lt;user&amp;gt;root&amp;lt;/user&amp;gt; &amp;lt;password&amp;gt;myPassword&amp;lt;/password&amp;gt; &amp;lt;database&amp;gt;myDatabase&amp;lt;/database&amp;gt; &amp;lt;!-- ... --&amp;gt; &amp;lt;/properties&amp;gt; properties 태그에 필요한 변수들을 설정하자. 필드명은 원하는 것으로 바꿔도 상관 없다. POM에 플러그인 설정하기 Cloud Tools에 대한 maven plugin 설정이 필요하다. 아래는 pom.xml의 snippet이다 &amp;lt;plugin&amp;gt; &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;maven-war-plugin&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;3.0.0&amp;lt;/version&amp;gt; &amp;lt;configuration&amp;gt; &amp;lt;webResources&amp;gt; &amp;lt;!-- in order to interpolate version from pom into appengine-web.xml --&amp;gt; &amp;lt;resource&amp;gt; &amp;lt;directory&amp;gt;${basedir}/src/main/webapp/WEB-INF&amp;lt;/directory&amp;gt; &amp;lt;filtering&amp;gt;true&amp;lt;/filtering&amp;gt; &amp;lt;targetPath&amp;gt;WEB-INF&amp;lt;/targetPath&amp;gt; &amp;lt;/resource&amp;gt; &amp;lt;/webResources&amp;gt; &amp;lt;/configuration&amp;gt; &amp;lt;/plugin&amp;gt; &amp;lt;plugin&amp;gt; &amp;lt;groupId&amp;gt;com.google.cloud.tools&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;appengine-maven-plugin&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;1.3.1&amp;lt;/version&amp;gt; &amp;lt;configuration&amp;gt; &amp;lt;deploy.promote&amp;gt;true&amp;lt;/deploy.promote&amp;gt; &amp;lt;deploy.stopPreviousVersion&amp;gt;true&amp;lt;/deploy.stopPreviousVersion&amp;gt; &amp;lt;/configuration&amp;gt; &amp;lt;/plugin&amp;gt; POM에 JDBC 라이브러리 추가production 환경에서는 아래 의존성이 사실 필요 없다. 하지만, local에서 테스트를 위해서는 필요하니 미리 추가해 두자 &amp;lt;dependency&amp;gt; &amp;lt;!-- Only used locally --&amp;gt; &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;5.1.42&amp;lt;/version&amp;gt; &amp;lt;/dependency&amp;gt; &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;com.google.cloud.sql&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;mysql-socket-factory&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;1.0.8&amp;lt;/version&amp;gt; &amp;lt;/dependency&amp;gt; 만일 앱엔진 프로젝트와 Cloud SQL 프로젝트가 서로 다르다면 아래 문서를 참조하여 추가 설정해야 한다.https://cloud.google.com/appengine/docs/standard/java/cloud-sql/using-cloud-sql-mysql#granting-access appengine-web.xml 설정 pom.xml의 properties는 컴파일 시점에 참조가 될 것이다. 그러면, property의 내용을 프로그램에서 참조하는 곳은 어디인가? (단순한 것인데 이부분을 이해 못해 한참 헤멨다. :sob:) 바로 appengine-web.xml에서 pom.xml의 properties를 참조하여 시스템 변수를 생성해 줄 수 있다. 이러한 흐름이 있기 때문에, local test 환경과 production 환경에 대한 세팅이 간결해지고 관리도 쉬워진다. appengine-web.xml 예시 &amp;lt;appengine-web-app xmlns=&quot;http://appengine.google.com/ns/1.0&quot;&amp;gt; &amp;lt;threadsafe&amp;gt;true&amp;lt;/threadsafe&amp;gt; &amp;lt;runtime&amp;gt;java8&amp;lt;/runtime&amp;gt; &amp;lt;service&amp;gt;cloudsql&amp;lt;/service&amp;gt; &amp;lt;system-properties&amp;gt; &amp;lt;property name=&quot;cloudsql&quot; value=&quot;jdbc:mysql://google/${database}?useSSL=false&amp;amp;amp;cloudSqlInstance=${INSTANCE_CONNECTION_NAME}&amp;amp;amp;socketFactory=com.google.cloud.sql.mysql.SocketFactory&amp;amp;amp;user=${user}&amp;amp;amp;password=${password}&quot; /&amp;gt; &amp;lt;/system-properties&amp;gt; &amp;lt;/appengine-web-app&amp;gt; system-properties는 구동되는 프로그램(앱엔진)의 시스템 환경변수로 등록된다. 그리고, 이 property에서 ${}로 작성되는 변수들은 pom.xml의 property 변수명을 참조하게 된다. 코드 샘플 @SuppressWarnings(&quot;serial&quot;) // With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required. @WebServlet(name = &quot;CloudSQL&quot;, description = &quot;CloudSQL: Write timestamps of visitors to Cloud SQL&quot;, urlPatterns = &quot;/cloudsql&quot;) public class CloudSqlServlet extends HttpServlet { Connection conn; @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { final String createTableSql = &quot;CREATE TABLE IF NOT EXISTS visits ( &quot; + &quot;visit_id SERIAL NOT NULL, ts timestamp NOT NULL, &quot; + &quot;PRIMARY KEY (visit_id) );&quot;; final String createVisitSql = &quot;INSERT INTO visits (ts) VALUES (?);&quot;; final String selectSql = &quot;SELECT ts FROM visits ORDER BY ts DESC &quot; + &quot;LIMIT 10;&quot;; String path = req.getRequestURI(); if (path.startsWith(&quot;/favicon.ico&quot;)) { return; // ignore the request for favicon.ico } PrintWriter out = resp.getWriter(); resp.setContentType(&quot;text/plain&quot;); Stopwatch stopwatch = Stopwatch.createStarted(); try (PreparedStatement statementCreateVisit = conn.prepareStatement(createVisitSql)) { conn.createStatement().executeUpdate(createTableSql); statementCreateVisit.setTimestamp(1, new Timestamp(new Date().getTime())); statementCreateVisit.executeUpdate(); try (ResultSet rs = conn.prepareStatement(selectSql).executeQuery()) { stopwatch.stop(); out.print(&quot;Last 10 visits:\\n&quot;); while (rs.next()) { String timeStamp = rs.getString(&quot;ts&quot;); out.println(&quot;Visited at time: &quot; + timeStamp); } } } catch (SQLException e) { throw new ServletException(&quot;SQL error&quot;, e); } out.println(&quot;Query time (ms):&quot; + stopwatch.elapsed(TimeUnit.MILLISECONDS)); } @Override public void init() throws ServletException { String url = System.getProperty(&quot;cloudsql&quot;); log(&quot;connecting to: &quot; + url); try { conn = DriverManager.getConnection(url); } catch (SQLException e) { throw new ServletException(&quot;Unable to connect to Cloud SQL&quot;, e); } } } 실행하기개발환경에서 테스트하기 mvn appengine:run Deploy 하기 mvn appengine:deploy IntelliJ에서 Google Cloud Tool 연동하기 를 잘 따라 했다면, IDEA에서 바로 run을 실행시켜도 된다. Test 및 deploy 환경 추가 설정여기까지 잘 따라 왔다면 deploy하여 앱엔진에서 Cloud SQL로 잘 접속이 되는 것을 확인할 수 있을 것이다.헌데, 문제가 있다. local testing환경에서 Cloud SQL로 직접 접속하여 테스트 할 수 없다는 것이다.그래서, 아래와 같은 설정을 추가한다.pom.xml에 profiles 추가&amp;lt;profiles&amp;gt; &amp;lt;profile&amp;gt; &amp;lt;id&amp;gt;local&amp;lt;/id&amp;gt; &amp;lt;properties&amp;gt; &amp;lt;mysql_url&amp;gt;&amp;lt;![CDATA[jdbc:mysql://your.ip:port/${database}?user=${user}&amp;amp;amp;password=${password}]]&amp;gt;&amp;lt;/mysql_url&amp;gt; &amp;lt;/properties&amp;gt; &amp;lt;/profile&amp;gt; &amp;lt;profile&amp;gt; &amp;lt;id&amp;gt;deploy&amp;lt;/id&amp;gt; &amp;lt;properties&amp;gt; &amp;lt;mysql_url&amp;gt;&amp;lt;![CDATA[jdbc:mysql://google/${database}?useSSL=false&amp;amp;amp;cloudSqlInstance=${INSTANCE_CONNECTION_NAME}&amp;amp;amp;socketFactory=com.google.cloud.sql.mysql.SocketFactory&amp;amp;amp;user=${user}&amp;amp;amp;password=${password}]]&amp;gt; &amp;lt;/mysql_url&amp;gt; &amp;lt;/properties&amp;gt; &amp;lt;/profile&amp;gt; &amp;lt;/profiles&amp;gt;로컬에서는 Cloud SQL의 connection url이 먹히지 않는다. 일반적이 mysql connection url을 그대로 사용하면 된다. 당연히, cloud sql instance는 방화벽이 열려 있어야 할 것이다. 주의 : xml에서 xml을 참조하기 때문에 escape char 처리가 문제가 된다. 위 예시처럼 그냥 CDATA를 사용하면 편하다. 그래도, pom.xml에서 &amp;amp;와 같은 특수기호는 escape 처리를 해 줘야 한다.appengine-web.xml 수정&amp;lt;system-properties&amp;gt; &amp;lt;property name=&quot;cloudsql&quot; value=&quot;${mysql_url}&quot; /&amp;gt;&amp;lt;/system-properties&amp;gt;이제 설정이 마무리 됐다. profile 기본설정을 local 로 해 두고 맘 편히 개발하자.끝!!참조 구글 공식 문서 : https://cloud.google.com/appengine/docs/standard/java/" }, { "title": "IntelliJ에 Google Cloud Tools 연동하기", "url": "/posts/cloud-tools-for-intellij/", "categories": "java, app engine, gcp, intellij", "tags": "app-engine, gcp, intellij, cloud-tools, java", "date": "2018-07-23 00:00:00 +0900", "snippet": " Intellij에서 Google Cloud Tools를 사용하려하고 찾아보니 구글문서가 깔끔하게 잘되어 있었다. 잊어버리기 전에 기록~헌데, 내가 가진 intellij가 2016버전이라 다소 삽질을 좀 하고.. 결국 2018버전으로 업그래이드 후 재구성.. :sob:문서를 좀 믿자~필요한 툴 IntelliJ IDEA, 2017 or higher!!! - ultimate edition은 모두 지원하지만 그 이하는 제한적임. Git Maven 3.1 or later왠만하면 gradle을 사용하려 하는데, GCP는 아직 maven을 더 잘 지원하는 듯 하다. 스트레스 받기 싫으면 아직은 GCP에서 gradle 사용하지 마시길..아래 내용은 IntelliJ 2018.1.6 Ultimate Edition 기준임Cloud Tools 설치 및 설정 Preferences &amp;gt; Plugins &amp;gt; Browse repositories 클릭 Google Cloud Tools 선택 및 설치 Google App Engine Integration 메세지에 대한 처리 위 처럼 설치하다보면 “Google App Engine Integration”을 disable 하겠냐는 메세지 창이 뜬다. 사실 이 녀석은 deprecate 됐다(https://cloud.google.com/tools/intellij/docs/migrate) . 구글 측에서는 Cloud Tools로 통합하여 관리하기 위해 버린 듯 하다. 이 녀석은 삭제도 안된다. 아무튼 disable하고 넘어가면 된다. IntelliJ restart JDK 설정 FIle &amp;gt; Project Structure (⌘ + ;) &amp;gt; Project Settings &amp;gt; Project에서 JDK 세팅이 제대로 됐는지 확인앱엔진 테스트 및 배포 환경 세팅Google App Engine Standard Local Server 세팅App Engine Standard를 기준입니다. Run &amp;gt; Edit Configurations 클릭 Run/Debug Configurations Dialog가 나타나면 왼쪽 상단 + 클릭하여 Google App Engine Standard Local Server 추가 Artifact to deploy 변경만일 드롭다운 박스에 해당 artifact가 나타나지 않은다면, 프로젝트 세팅(⌘ + ;) 에서 artifacts를 추가해 줘야 한다. App Engine Host/Port 를 원하는 것으로 설정 환경변수 세팅 GOOGLE_CLOUD_PROJECT 변수에 현재 사용중인 프로젝트 ID를 입력한다 GOOGLE_APPLICATION_CREDENTIALS 변수에 IAM에서 서비스 키 json 파일을 받고 그 경로를 입력한다여기까지 잘 됐다면 로컬에서 실행 및 디버깅이 가능하게 된다.Deploy 환경 세팅 메인 메뉴에서 Tools &amp;gt; Google Cloud Tools &amp;gt; Deploy to App Engine 클릭하면 아래와 같은 Dialog 박스가 나타난다 ... 버튼을 클릭하면 Google App Engine 팝업창이 나타나고, 여기에서 “Google App Engine” 서버를 추가하자. 만일 서버 목록이 없다면 + 버튼을 클릭하여 추가하면 된다 서버를 추가하면 아래 그림처럼 Dialog 화면이 바뀌게 된다. Deployment 필드에 원하는 내용을 선택하자 Community Edition을 사용중이면 Maven이나 Gradle artifact만 사용할 수 있다고 하는데.. 별 의미가 없다. (Community 버전을 살껄.. :sob:) Project 필드에서 원하는 프로젝트를 선택하자. 만일, IntelliJ에서 처음 세팅하는 것이라면, 구글 accounnt로 로그인하라는 창이 나타날 것이다. 인증을 진행하면 내 계정에 묶여 있는 모든 프로젝트가 나타난다. (이건 참 편리하다) 다른 필드들도 필요에 따라 수정하면 된다. 이제 준비는 끝났다. Run을 클릭하면 앱엔진에 내 사이트가 나타난다~ 와~~기타Tools &amp;gt; Google Cloud Tools 메뉴에 보면 여러가지 부가 기능들이 들어 있다. 유용한 것이 좀 있으니 자주 활용하자!참조 구글 공식 문서 : https://cloud.google.com/tools/intellij/docs/how-to" }, { "title": "자바 어플리케이션을 gcp app engine으로 구성하기 1 - 예제 실행해 보기", "url": "/posts/java-app-engine-in-gcp-1/", "categories": "java, app engine", "tags": "app-engine, gcp, java", "date": "2018-07-11 00:00:00 +0900", "snippet": " GCP에서 서비스 구축할 일이 있어서 이것저것 공부중입니다. 생각보다 쉬운데.. 문서를 이해하는 시간이 더 오래 걸리는군요.이 시리즈는 아래 3개로 구성하였습니다. intelliJ를 사용하려고 이것저것 테스트하다보니 포스팅 시간이 오래걸리네요.관련 포스팅 IntelliJ에 Google Cloud Tools 연동하기 자바 어플리케이션을 gcp app engine으로 구성하기 1 - 예제 실행해 보기 - 이 문서 자바 어플리케이션을 gcp app engine으로 구성하기 2 - CloudSQL 연동목표 Hello World 샘플 프로그램을 통해 앱엔진 맛보기예제로 앱 엔진 구성하기standard environment 에서 앱엔진 구성필요한 툴Java SE 8 Development Kit(JDK) - 앱엔진은 7, 8만 지원GitMaven선행 작업 Google Cloud SDK 다운로드 및 설치 gcloud 명령으로 command-line 환경 설정 gcloud init gcloud auth application-default login 앱엔진 컴포넌트 설치 gcloud components install app-engine-java 최신 버전으로 업데이트 gcloud components update 샘플 실행해보기Hellow Wordl app 다운로드아래 명령을 사용하여 git에서 샘플 프로그램 다운로드하면 GCP의 getting started 샘플 프로그램을 모두 받을 수 있다.git clone https://github.com/GoogleCloudPlatform/getting-started-java.git`샘플 코드가 들어 있는 디렉토리로 이동cd getting-started-java/appengine-standard-java8/helloworld`로컬에서 실행해 보기 Jetty Maven plugin을 통해 로컬에서 Jetty server 실행 mvn appengine:run 브라우저에서 확인 http://localhost:8080 브라우저에서 Hellow World 메세지가 보이면 정상 터미널에서 Ctrl+C 로 web server 종료앱엔진에 배포하고 실행해 보기 getting-started-java/appengine-standard-java8/hellowworld 디렉토리에서 아래 명령을 통해 배포 mvn appengine:deploy 아래 명령을 통해 브라우저 실행하면 http://my_project_id.appspot.com으로 접속하여 결과를 확인 할 수 있음 gcloud app browse 결과 확인브라우저가 실행되면서 아래와 같이 화면이 나타나면 성공~코드 리뷰Hellow World는 가장 단순하게 만들어진 앱엔진 앱이다. 따라서 기본적인 구조를 파악하기 위해서는 아주 제격이다.HelloAppEngine.java소스는 이 파일 하나로 구성되어 있다. 단일 버전, 다인 서비스로 구성되어 있는 단순한 구조이기 때문에 이 파일 하나로도 충분하다.import com.google.appengine.api.utils.SystemProperty; import java.io.IOException; import java.util.Properties; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; // With @WebServlet annotation the webapp/WEB-INF/web.xml is no longer required. @WebServlet(name = &quot;HelloAppEngine&quot;, value = &quot;/hello&quot;) public class HelloAppEngine extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { Properties properties = System.getProperties(); response.setContentType(&quot;text/plain&quot;); response.getWriter().println(&quot;Hello App Engine - Standard using &quot; + SystemProperty.version.get() + &quot; Java &quot; + properties.get(&quot;java.specification.version&quot;)); } public static String getInfo() { return &quot;Version: &quot; + System.getProperty(&quot;java.version&quot;) + &quot; OS: &quot; + System.getProperty(&quot;os.name&quot;) + &quot; User: &quot; + System.getProperty(&quot;user.name&quot;); } }소스는 사실 별게 없다. 서블릿 프로그램을 많이 해본 분들이라면 그냥 봐도 알만한 내용..이쪽 경험이 별로 없는 필자는 @WebServlet annotation을 처음 본지라, web.xml 설정하기 위한 번거로움이 필요 없다는 것을 새로 안 정도~브라우저에서 hello 진입점으로 들어오게 되면 doGet 메소드가 실행되고 결과를 출력한다. getInfo메소드는 index.jsp에서 호출하는데, 처음 페이지를 만들때 사용된다(쉽네~)pom.xmlmaven plugin을 사용하기 위해 아래 내용이 추가되어 있다.&amp;lt;plugin&amp;gt; &amp;lt;groupId&amp;gt;com.google.cloud.tools&amp;lt;/groupId&amp;gt; &amp;lt;artifactId&amp;gt;appengine-maven-plugin&amp;lt;/artifactId&amp;gt; &amp;lt;version&amp;gt;1.3.1&amp;lt;/version&amp;gt; &amp;lt;/plugin&amp;gt;appengine-web.xmlruntime을 위한 java8과 같이 꼭 필요한 설정이 들어 있다. 이 파일에 대한 설명은 appengine-web.xml Reference 를 참조하자&amp;lt;appengine-web-app xmlns=&quot;http://appengine.google.com/ns/1.0&quot;&amp;gt; &amp;lt;runtime&amp;gt;java8&amp;lt;/runtime&amp;gt; &amp;lt;threadsafe&amp;gt;true&amp;lt;/threadsafe&amp;gt; &amp;lt;/appengine-web-app&amp;gt;Gradle로 실행하기소스를 보니 gradle 환경설정이 되어 있다. 그래서 간단히 실행해보니 동일하게 잘 동작한다.로컬에서 실행./gradlew appengineRunDeploy./gradlew appengineDeploy앱엔진 관련 명령솔직히 다 모르겠다~ :-(&#39;appengineDeploy&#39; &#39;appengineDeployCron&#39;&#39;appengineDeployDispatch&#39;&#39;appengineDeployDos&#39;&#39;appengineDeployIndex&#39;&#39;appengineDeployQueue&#39;&#39;appengineRun&#39;&#39;appengineShowConfiguration&#39;&#39;appengineStage&#39;&#39;appengineStart&#39;&#39;appengineStop&#39;참조 구글 공식 문서 : https://cloud.google.com/appengine/docs/standard/java/ 앱엔진 개발 환경 세팅 : https://cloud.google.com/appengine/docs/standard/java/building-app/environment-setup" }, { "title": "gradle로 jar파일 로컬 저장소에 설치하고 재사용하기", "url": "/posts/gradle-local-repository-install/", "categories": "java", "tags": "jar, gradle, gradle-install, java", "date": "2018-07-09 00:00:00 +0900", "snippet": "gradle로 jar파일 로컬 저장소에 설치하고 재사용하기내가 만든 jar를 다른 프로젝트에서 사용하려는 경우는 자주 있습니다.헌데, gradle로 해 보려니 이것도 많이 찾아봐야 하는 군요.(물론 구글링 잘하면 단 1분만에 끝낼 수 있습니다 :sob:install 하려는 프로젝트build.gradle 파일에서 maven plugin을 허용하고, group, version을 세팅해 줍니다.apply plugin: &quot;maven&quot;group = &quot;foo&quot;version = &quot;1.0&quot; 그런 다음, gradle install 명령을 입력하면 됩니다.사용하려는 프로젝트build.gradle 파일에서 로컬저장소와 의존성을 설정해 줍니다.repositories { mavenLocal()}dependencies { compile &quot;foo:sdk:1.0&quot;}그런 다음, gradle build 명령을 입력하면 됩니다.참조https://stackoverflow.com/questions/6122252/gradle-alternate-to-mvn-install" }, { "title": "curl을 사용하여 HTTP Request 시간 구하기", "url": "/posts/time-check-http-request-with-curl/", "categories": "tools, http", "tags": "curl, http", "date": "2018-07-04 00:00:00 +0900", "snippet": " curl 로 타이밍 체크하는 방법을 모르는 분들이 꽤 많습니다. 물론 저도 몰랐구요헌데 찾아보니 나오네요. 잊어버리기 전에 기록합니다. :sweat_smile:curl을 사용하여 HTTP Request 시간 구하기아래와 같은 내용을 파일로 저장합니다. 저는 curl-format.txt로 했습니다. time_namelookup: %{time_namelookup}\\n time_connect: %{time_connect}\\n time_appconnect: %{time_appconnect}\\n time_pretransfer: %{time_pretransfer}\\n time_redirect: %{time_redirect}\\n time_starttransfer: %{time_starttransfer}\\n ----------\\n time_total: %{time_total}\\n명령어를 다음과 같이 입력합니다.curl -w &quot;@curl-format.txt&quot; --compress -o /dev/null -s http://www.naver.com결과는 다음과 같네요 time_namelookup: 0.012691 time_connect: 0.015969 time_appconnect: 0.000000 time_pretransfer: 0.016212 time_redirect: 0.000000 time_starttransfer: 0.018516 ---------- time_total: 0.018667단위는 밀리세컨드입니다. 네이버 정말 빠르네요.. :scream:시간상 자세한 내용은 아래 참고를 참고~참고 https://overloaded.io/timing-http-requests-curl https://curl.haxx.se/docs/manual.html" }, { "title": "gcp 유용한 정보들", "url": "/posts/gcp-favorate-url/", "categories": "gcp", "tags": "gcp, tutorial, compute-engine, information, cost-calculator, web-push, fcm", "date": "2018-06-25 00:00:00 +0900", "snippet": "gcp 관련 유용한 정보들 compute engine tutorials https://cloud.google.com/compute/docs/tutorials 구글 전체 api list https://developers.google.com/apis-explorer/#p/ 구글 api client library https://developers.google.com/api-client-library/?authuser=2&amp;amp;hl=ko gcp 가격 계산기 https://cloud.google.com/products/calculator/ Questions of google api explorer https://stackoverflow.com/questions/tagged/google-apis-explorer FCM Cloud Messaging https://firebase.google.com/docs/cloud-messaging/?authuser=2&amp;amp;hl=ko Web Push Notification https://developers.google.com/web/fundamentals/push-notifications/?hl=ko " }, { "title": "embulk mongodb output plugin 개발", "url": "/posts/embulk-output-mongodb_nest/", "categories": "tools, embulk", "tags": "embulk, embulk-output-mongodb_nest, mongodb", "date": "2018-05-31 00:00:00 +0900", "snippet": " embulk-output-mongodb 플러그인이 공식사이트에서 링크가 죽었군요. gem repository 에서 install은 가능하지만, 회사에서 사용하기 좀 뭐 해서 그냥 만들어 봤습니다.참.. 쉽네요 😉개발 방법 embulk 신규 프로젝트 생성 프로젝트 빌딩 작업결과 확인 mongodb client 코드 작성1. embulk로 신규 프로젝트 생성embulk가 기본적인 플러그인 프로젝트의 템플릿 코드 및 프로젝트 자체를 만들어 줍니다. 상당히 간편하더군요. java와 ruby 두가지이니 입맛에 맞게 선택해서 만들면 되구요.저는 java로 만들어 봤습니다.아래 명령으로 간단히 생성 됩니다.$ embulk new &amp;lt;type&amp;gt; &amp;lt;name&amp;gt;type은 아래와 같습니다. type description example java-input Java record input plugin mysql java-output Java record output plugin mysql java-filter Java record filter plugin add-hostname java-file-input Java file input plugin ftp java-file-output Java file output plugin ftp java-parser Java file parser plugin csv java-formatter Java file formatter plugin csv java-decoder Java file decoder plugin gzip java-encoder Java file encoder plugin gzip ruby-input Ruby record input plugin mysql ruby-output Ruby record output plugin mysql ruby-filter Ruby record filter plugin add-hostname ruby-parser Ruby file parser plugin csv ruby-formatter Ruby file formatter plugin csv java output plugin을 만들거니 다음과 같이 입력하였습니다.$ embulk new java-output embulk_nestplugin 명칭을 “embulk_nest”로 잡았습니다. 나중에 실행시킬때는 “embulk-output-embulk_nest”로 될 것이기에, 중간에 “_“를 넣었습니다.2. 프로젝트 빌딩간단히 프로젝트가 생성되면, 다음 명령을 통해 빌딩할 수 있습니다.$ cd embulk-output-embulk_nest$ ./gradlew packagegradle을 사용하고 있어서 intellij에서 직접하려 했는데, 세팅이 잘 안 맞네요.그냥 커맨드 라인에서 작업하는 것이 편합니다.3. 작업 결과 확인패키지가 만들어졌으면 작업 내용을 확인할 수 있습니다.$ embulk run -L ./embulk-output-mongodb_nest config.ymlconfig.yml을 대충 만들어서 돌려보니 그냥 잘 돌아 갑니다.물론 아무 작업도 안하지만서도..여기까지 됐으면 거의 끝났다고 보면 됩니다.4. mongodb client 코드 작성MongodbNestOutputPlugin.java 수정사실 mongodb를 사용해 보지 않아서 오히려 여기부터 좀 시간을 잡아먹었습니다. 애초부터 mysql같은 플랫한 데이터를 sub document가 가능하게 mongodb로 넣기 위한 작업이었기에 좀 헤맸네요.아무튼 아래 소스에 코딩을 시작하면 됩니다.파일 위치 : src/org/embulk/*실제 수정할 파일은 다음과 같이 만들어져 있네요.src/main/java/org/embulk/output/mongodb_nest/MongodbNestOutputPlugin.java탬플릿에는 몇개의 메소드와 인터페이스가 만들어져 있습니다. 메소드 기능 PluginTask config.yml의 설정값을 입력받기 위한 인터페이스 transaction transaction을 수행하기 위한 기본 메소드 resume resume을 구현하기 위한 메소드 cleanup cleanup을 구현하기 위한 메소드 open 실제 output plugin의 작업을 수행하기 위한 메소드 mongodb의 기본적인 설정 정보를 담기 위해 PluginTask interface를 다음과 같이 수정하였습니다. public interface DefineChildDocument extends Task { @Config(&quot;name&quot;) public String getName(); @Config(&quot;field&quot;) public String getField(); } public interface PluginTask extends Task { @Config(&quot;collection&quot;) public String getCollection(); @Config(&quot;host&quot;) public String getHost(); @Config(&quot;port&quot;) @ConfigDefault(&quot;27017&quot;) public int getPort(); @Config(&quot;database&quot;) public String getDatabase(); @Config(&quot;user&quot;) public String getUser(); @Config(&quot;password&quot;) public String getPassword(); @Config(&quot;key&quot;) public String getKey(); @Config(&quot;child&quot;) @ConfigDefault(&quot;null&quot;) public Optional&amp;lt;List&amp;lt;DefineChildDocument&amp;gt;&amp;gt; getChild(); @Config(&quot;bulk_size&quot;) @ConfigDefault(&quot;1000&quot;) public int getBulkSize(); } 참고할 점은 선택 옵션인 경우는 “@ConfigDefault”를 작성해 주면 된다는 것입니다. 없는 경우는 필수 항목이 되어 반드시 config.yml에 있어야 합니다.주의할 것은, 선택항목인데, 기본값이 필요 없는 경우는 “null”로 주면 됩니다. “” 이런식으로 주면 에러가 나더군요mongodb 접속 정보를 url로 입력할 수도 있는데, 문제는 ‘@’와 같은 특수문자가 기본 정보에 들어 있으면 mongodb client에서 에러가 납니다. url encoding을 하면 되는데, 좀 더 편리하게 하려고, 필드들을 모두 잘라서 입력하도록 했습니다.기본적인 접속 정보와 함께, child라는 필드를 추가했습니다.nested 되는 도큐멘트를 정의하기 위함이구요. name으로 sub document 필드명을, field로 input에서 받은 필드명을 입력하도록 하였습니다.같은 name이 설정될 경우는 하나의 sub document에 들어가도록 구성하였습니다.두번째로 bulk_size를 만들었는데, 시스템 자원에 따라 알아서 쓰면 되겠지요.config는 잡았으니.. 실제 수행할 몸체를 구현하면 되겠지요.open 메소드를 아래와 같이 수정하였습니다.@Override public TransactionalPageOutput open(TaskSource taskSource, Schema schema, int taskIndex) { PluginTask task = taskSource.loadTask(PluginTask.class); return new PluginPageOutput(task, schema); }PluginPageOutput.java 추가PluginPageOutput 클래스는 TransactionalPageOutput 클래스를 상속받아 아래와 같이 구현하였습니다.package org.embulk.output.mongodb_nest; import com.mongodb.BasicDBObject; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.ReplaceOneModel; import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.WriteModel; import org.bson.Document; import org.embulk.config.TaskReport; import org.embulk.spi.*; import org.embulk.spi.time.Timestamp; import org.slf4j.Logger; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; public class PluginPageOutput implements TransactionalPageOutput { private static final Logger logger = Exec.getLogger(MongodbNestOutputPlugin.class); private final MongodbNestOutputPlugin.PluginTask task; private final Schema schema; private final PageReader pageReader; private MongoClient mongo; private MongoDatabase db; private MongoCollection&amp;lt;Document&amp;gt; collection; PluginPageOutput(MongodbNestOutputPlugin.PluginTask task, Schema schema) { this.pageReader = new PageReader(schema); this.schema = schema; this.task = task; String connectionStr = &quot;mongodb://&quot;; if (task.getUser() != null) { connectionStr += task.getUser(); try { connectionStr += &quot;:&quot; + URLEncoder.encode(task.getPassword(), &quot;UTF-8&quot;) + &quot;@&quot;; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } connectionStr += task.getHost() + &quot;:&quot; + task.getPort() + &quot;/&quot; + task.getDatabase(); this.mongo = MongoClients.create(connectionStr); this.db = this.mongo.getDatabase(task.getDatabase()); this.collection = this.db.getCollection(task.getCollection()); } @Override public void add(Page page) { pageReader.setPage(page); List&amp;lt;WriteModel&amp;lt;Document&amp;gt;&amp;gt; replaceModel = new ArrayList&amp;lt;&amp;gt;(); while (pageReader.nextRecord()) { BasicDBObject doc = new BasicDBObject(); for (int i = 0; i &amp;lt; schema.getColumnCount(); i++) { String t = schema.getColumnName(i); Class&amp;lt;?&amp;gt; type = schema.getColumnType(i).getJavaType(); if (pageReader.isNull(i)) { doc.append(t, null); } else if (type.equals(boolean.class)) { doc.append(t, pageReader.getBoolean(i)); } else if (type.equals(double.class)) { doc.append(t, pageReader.getDouble(i)); } else if (type.equals(long.class)) { doc.append(t, pageReader.getLong(i)); } else if (type.equals(String.class)) { doc.append(t, pageReader.getString(i)); } else if (type.equals(Timestamp.class)) { doc.append(t, new java.sql.Timestamp(pageReader.getTimestamp(i).toEpochMilli())); } } if (task.getChild() != null) { doc = transformDocument(doc); } replaceModel.add(new ReplaceOneModel&amp;lt;&amp;gt;( new Document(task.getKey(), doc.get(task.getKey())), new Document(doc), new ReplaceOptions().upsert(true)) ); if(replaceModel.size() % task.getBulkSize() == 0) { collection.bulkWrite(replaceModel); replaceModel.clear(); } } if (replaceModel.size() &amp;gt; 0) { collection.bulkWrite(replaceModel); } } private BasicDBObject transformDocument(BasicDBObject doc) { for (MongodbNestOutputPlugin.DefineChildDocument cd : task.getChild().get()) { String name = cd.getName(); String field = cd.getField(); BasicDBObject subdoc = new BasicDBObject(); Object exists = doc.get(name); if (exists != null) { subdoc.putAll((Map) exists); } subdoc.append(field, doc.remove(field)); doc.append(name, subdoc); } return doc; } @Override public void finish() { } @Override public void close() { this.mongo.close(); } @Override public void abort() { } @Override public TaskReport commit() { return null; } }음.. 이노무 블로그는 소스 폴딩이 안돼!! 누가 도움 좀.. :sob:TransactionalPageOutput 클래스를 상속받아 코드를 생성하여, 몇몇 필수 메소드를 override하여 작성합니다.생성자에서 mongodb에 접속하도록 하였고, add method에서 필요한 작업을 수행했습니다.sub document를 만들기 위해, transformDocument 메소드를 추가해서 구조를 변형하였습니다.사실, 코딩 능력이 일천하여 하나하나 설명하기가 좀.. 그러네요. (뭐.. 나만 알면 되지~ :kissing_smiling_eyes:config.yml 작성테스트를 위해서, embulk의 샘플 csv를 input으로하여 작성해 봤습니다.in: type: file path_prefix: /Users/focuschange/google/program/embulk/./try1/csv/sample_ decoders: - {type: gzip} parser: charset: UTF-8 newline: LF type: csv delimiter: &#39;,&#39; quote: &#39;&quot;&#39; escape: &#39;&quot;&#39; null_string: &#39;NULL&#39; trim_if_not_quoted: false skip_header_lines: 1 allow_extra_columns: false allow_optional_columns: false columns: - {name: id, type: long} - {name: account, type: long} - {name: time, type: timestamp, format: &#39;%Y-%m-%d %H:%M:%S&#39;} - {name: purchase, type: timestamp, format: &#39;%Y%m%d&#39;} - {name: comment, type: string}out: type: mongodb_nest host: 999.999.999.999# port: 27017 database: focus user: focus password: focus@123.456.789 collection: PluginTest key: account child: - {name: mychild, field: time} - {name: yourchild, field: comment} - {name: mychild, field: purchase}실제 로컬에서 수행해 보니, mongodb에 다음과 같이 들어갑니다.child 필드로 작성한 mychild, yourchild 모두 잘 들어갔네요.아무튼 간단히 작성해서 테스트 해 보니 잘 들어갑니다.5. 배포하는김에 젬 배포까지 해 봤습니다.https://rubygems.org 에 가입하고, console에서 배포하기 위해 인증까지 받았습니다.console 인증은 그냥 다음처럼 했습니다. $ gem pushEnter your RubyGems.org credentials.Don&#39;t have an account yet? Create one at https://rubygems.org/sign_up Email: focuschange@gmail.comPassword:Signed in.ERROR: While executing gem ... (Gem::CommandLineError) Please specify a gem name on the command line (e.g. gem build GEMNAME)email과 암호를 입력하니 siged in 성공하고, “~/.gem/credentials” 파일이 생성됩니다. 뭐.. 다음 에러야 무시하고.. 인증받았으면 됐고~아래 명령으로 gem push 하면 빌딩 및 배포가 이루어집니다,$ ./gradlew gemPush........&amp;gt; Task :gem Successfully built RubyGem Name: embulk-output-mongodb_nest Version: 0.1.0 File: embulk-output-mongodb_nest-0.1.0.gem&amp;gt; Task :gemPushPushing gem to https://rubygems.org...Successfully registered gem: embulk-output-mongodb_nest (0.1.0)필요한 모듈들 다운받아 패키징하고, 위에 메세지처럼 gem에 올라가네요.https://rubygems.org 에서 검색해보니, 나옵니다. 와~~6. 결론embulk는 사용하면 할 수록 참 편하다는 생각이 드네요. custom도 상당히 쉬운 편이구요.아직 몽고DB를 능숙하게 사용하지 못하기 때문에, 코드가 좀 지저분하지만, 점진적으로 가다듬으면 쓰는데 나쁘지 않을 것 같습니다.오히려 회사에서 방화벽 열고, 이것저것 준비하는 시간이 더 걸립니다.참고 소스코드 : https://github.com/focuschange/embulk-output-mongodb_nest ruby gem : https://rubygems.org/gems/embulk-output-mongodb_nest embulk customization : http://www.embulk.org/docs/customization.html" }, { "title": "개발에 유용한 툴들", "url": "/posts/favorite-tools/", "categories": "tools", "tags": "developer-tool, 개발툴", "date": "2018-05-18 00:00:00 +0900", "snippet": " 가끔 장비를 교체하는 경우 전에 설치했던 툴들이 기억이 나지 않을 때가 있습니다. 세월이 갈수록 점점.. 더..그래서 내가 사용하는 툴 목록을 정리해 두려 합니다.DB ClientMYSQL MySQL WorkBench - https://www.mysql.com/products/workbench/ :-1: HeidiSQL - https://www.heidisql.com/ :+1:Vertica DBeaver - https://dbeaver.io/IDE &amp;amp; Editorprogramming IDE jet brains All Products pack - https://www.jetbrains.com/store/?fromMenu#edition=commercial – including Intellij, ReSharper and other IDEs – intelliJ, PhpStorm정도 쓴다면 그냥 all pack을 사는 것도.. :smirk:Markdown Editor stackedit.io - web editor, https://stackedit.io markdown emoji code - https://gist.github.com/rxaviers/7360908 :+1:Text Editor sublime text 3 - https://www.sublimetext.com/ :+1:Terminal iterm - https://www.iterm2.com/ putty - https://www.putty.org/Performance Test nGrinder : http://naver.github.io/ngrinder/ETC Tools calculator - https://numi.io/ only mac :+1: android emulator - https://kr.bignox.com/ :+1: screen capture -https://evernote.com/intl/ko/products/skitch tcp capture - https://www.wireshark.org/ SCM - https://github.com/ countdown - https://itunes.apple.com/kr/app/countdown-widget/id506996014?mt=12 rest api - https://www.getpostman.com/ file transfer(any platform) - https://send-anywhere.com/ETC Configure vim : https://github.com/amix/vimrc&quot; syntax Highlightingif has(&quot;syntax&quot;) syntax onendifcolorscheme evening&quot; set cindentset nuset ts=4set nocp &quot; &#39;compatible&#39; is not setfiletype plugin on &quot; plugins are enabledlet g:netrw_banner = 0let g:netrw_liststyle = 3let g:netrw_browse_split = 4let g:netrw_altv = 1let g:netrw_winsize = 25augroup ProjectDrawer autocmd! autocmd VimEnter * :Vexploreaugroup END" }, { "title": "embulk를 사용한 vertica to bigquery 방법", "url": "/posts/embulk-bigquery-output/", "categories": "tools, embulk", "tags": "embulk, bigquery, vertica, etl", "date": "2018-05-11 10:59:10 +0900", "snippet": " 다른 플랫폼간에 데이터 전송이나 ETL을 위해서 여러가지 툴이 존재한다고 합니다. 그 중 embulk를 소개받게 되어 사용성 테스트를 해 봤습니다. 물론 많은 툴을 써본 경험은 없으나, embulk는 물건인 듯 싶습니다. 설치부터 데이터 전송까지 단계적으로 기록해 보려 합니다.macOS High Sierra 기준입니다.1. embulk 설치하고 실행하기embulk를 설치하는 것은 아주 간단합니다.아래와 같이 입력하면 설치가 끝납니다.curl --create-dirs -o ~/.embulk/bin/embulk -L &quot;https://dl.embulk.org/embulk-latest.jar&quot;chmod +x ~/.embulk/bin/embulkecho &#39;export PATH=&quot;$HOME/.embulk/bin:$PATH&quot;&#39; &amp;gt;&amp;gt; ~/.bash_profile2. embulk 예제 실행“embulk example” 명령은 샘플용 csv 파일을 생성해 줍니다embulk example ./try1embulk guess ./try1/seed.yml -o config.ymlembulk preview config.ymlembulk run config.yml위 내용에서 “embulk example ./try1”을 실행하면 샘플용 데이터를 ./try1으로 생성해 줍니다.csv파일로 만들어주지만, 실제는 gz으로 압축되어 있습니다.두번째 줄은 seed.yml파일을 사용하여 config.yml을 만들어 줍니다.embulk를 사용하면서 좋았던 것이 guess 명령입니다.간단한 것은 guess명령으로 config.yml을 생성해 주니 생각보다 편리합니다.그러나, 항상 잘 만들어주지 못합니다.(아니, 거의 못만듭니다. ㅠㅠ)preview는 말 그대로 preview이고, run 명령을 통해 config.yml의 설정들을 실행 시킵니다.3. embulk 플러그인 설치하기example은 말그대로 예제일 뿐이고, 우리가 해 볼 것은 버티카에서 빅쿼리로 데이터를 전송하는 것이기 때문에, 해당하는 플러그인을 설치해야 합니다.데이터를 뽑아낼 플랫폼은 input이고, 데이터를 출력할 플랫폼은 output입니다. 다분히 embulk입장에서 input과 output을 정의한 것이고, 플랫폼들이 다 다르기 때문에, 플러그인으로 해당하는 in/out에 맞게 설치해 줘야 합니다.input/output 플러그인 목록은 아래 url로 확인할 수 있습니다.embulk 플러그인 목록다음 명령을 통해 플러그인을 간단히 설치할 수 있습니다.기본 명령 : embulk gem install &amp;lt;name&amp;gt;input/output에 따라 name 값을 “embulk-input-command” 또는 “embulk-output-command”로 입력합니다.“embulk gem” 명령은 ruby gem 명령을 wrapping한 것으로 보입니다.embulk 플러그인을 찾으려면 아래 명령으로 가능합니다.embulk gem search -rd embulk-output설치된 플러그인 리스트를 보려면 아래 처럼 입력합니다.embulk gem listembulk upgrade는 아래 명령으로 가능합니다.embulk selfupdate - 최신 버전으로 업그래이드embulk selfupdate x.y.z - x.y.z 버전으로 업그래이드.가끔 embulk 버전과 플러그인 버전을 맞춰야 하는 경우가 있습니다.이런 경우 위의 명령이 도움이 됩니다.4. BigQuery output 플러그인 사용법bigquery output plugin 설치는 아래와 같습니다.embulk gem install embulk-output-bigquerybigquery를 사용하려면 당연한 얘기겠지만, 인증을 받아야 합니다.GCP 서비스계정을 새로 만들고 Json 형태로 받습니다.(p12형태도 가능하지만 스킵하겠습니다)gcp의 iam에서 서비스 계정을 만들 때, 키 유형을 반드시 json으로 선택합니다.이후, 생성된 json을 로컬에 저장합니다.기본적인 config는 아래와 같습니다.in: type: file path_prefix: /embulk/try1/csv/sample_ decoders: - {type: gzip} parser: charset: UTF-8 newline: LF type: csv delimiter: &#39;,&#39; quote: &#39;&quot;&#39; escape: &#39;&quot;&#39; null_string: &#39;NULL&#39; trim_if_not_quoted: false skip_header_lines: 1 allow_extra_columns: false allow_optional_columns: false columns: - {name: id, type: long} - {name: account, type: long} - {name: time, type: timestamp, format: &#39;%Y-%m-%d %H:%M:%S&#39;} - {name: purchase, type: timestamp, format: &#39;%Y%m%d&#39;} - {name: comment, type: string}out:type: bigquerymode: appendauth_method: json_keyjson_keyfile: /embulk/key/your-keyfile-name.jsonproject: your-project-iddataset: tmptable: embulk_testauto_create_dataset: trueauto_create_table: truecolumn_options:- {name: id, type: INTEGER}- {name: account, type: FLOAT}- {name: time, type: STRING, timestamp_format: &#39;%Y-%m-%d %H:%M:%S&#39;, timezone: &quot;Asia/Tokyo&quot;}input으로는 샘플로 생성한 csv파일을 사용하였습니다.output으로 bigquery로 직접 입력하도록 하였는데, 데이터셋이나 테이블 자동생성 옵션을 두었는데, 잘 되는 군요5. vertica input 플러그인 사용법vertica input plugin 설치명령은 아래와 같습니다.embulk gem install embulk-input-vertica기본적인 config는 다음과 같습니다.in:type: verticahost: 35.194.97.115user: dbadminpassword: dbadmindatabase: vdbquery: | SELECT employee_key employee_gender, courtesy_title, employee_first_name, employee_middle_initial, employee_last_name, employee_age, hire_date, employee_street_address, employee_city, employee_state, employee_region, job_title, reports_to, salaried_flag, annual_salary, hourly_rate, vacation_days from employee_dimensioncolumn_options: hire_date: {type: string, value_type: string, timestamp_format: &quot;%Y-%m-%d&quot;, timezone: &quot;Asia/Tokyo&quot;}out: type: stdoutconfig.yml 구성 시 주의할 것은 date, timestamp type에 대한 형 변환입니다. column_options에 해당하는 필드를 정의해 주면 되구요.아래와 같이 timestamp를 변형해 주니 bigquery에서도 동일한 type으로 잘 들어가는 군요reg_dt: {value_type: string, timestamp_format: &quot;%Y-%m-%d %H:%M%S&quot;, timezone: &quot;+0900&quot;}주의할 점은 vertica의 date, timestamp는 value_type을 반드시 string 타입으로 해 주어야 합니다.embulk가 내부에서 string으로 처리하는 것 같습니다.timestamp type은 ruby의 strftime format을 따릅니다. (참고 : https://docs.ruby-lang.org/en/2.4.0/Date.html#method-i-strftime)Troubleshootingembulk-output-bigquery를 0.4.6에서 0.4.7로 업그래이드 할 때config.yml에 location 필드가 추가되야 합니다.(옵션이라면서 잘 안됩니다)google-api-client도 0.20.1로 업그래이드 되야 합니다.설치 방법embulk gem install google-api-clientvertica에서 bigquery로 직접 전송할 때 csv 포맷인식이 잘 안되는 경우특별한 옵션을 주지 않는 한 중간 변환 및 전송을 위해서 embulk는 csv 파일을 사용합니다. 보통 /tmp 디렉토리 아래에 임시파일을 생성하는데요. csv파일을 아무리 잘 만들어도 전송 시 오류가 발생하는 경우가 있습니다.이때는, 다음 옵션을 사용합니다. 물론 embulk-output-bigquery config에 해당합니다.source_format: NEWLINE_DELIMITED_JSONbigquery에서는 그냥 필수로 사용하는 것이 좋습니다. 헌데, csv에 비해 크기가 10배 가까이 늘어납니다. 그래서, 압축옵션을 다음과 같이 넣어 주는 것이 좋습니다.compression: GZIP 압축옵션 사용하지 않았다가 네트워크 대역폭 다 잡아먹었다고 욕먹었습니다. ㅠ_ㅠinput data가 커서 timeout이 발생하는 경우용량이 너무 크다보니 다음과 같은 에러가 발생하는 군요org.jruby.exceptions.RaiseException: (TransmissionError) execution expired이럴 경우는 아래 옵션들을 추가합니다.open_timeout_sec : 7200send_timeout_sec : 7200read_timeout_sec : 7200vertica select 절 구분자가 빠졌을 경우이건 제가 vertica를 몰라서 생긴 문제였는데요. select 절에 구분자가 빠지면 word가 exact match되는 필드를 그냥 가져옵니다.(정확한 표현은 아닙니다)이유는, mysql의 ‘key as name” 이 버티카에서는 “key name”으로 사용됩니다.콤마(,)가 빠져서 bigquery에 필드가 하나 없어진 원인을 찾는데 한참 삽질했네요.참조 embulk plugins referrence : http://www.embulk.org/plugins/ embulk bult-in configuration : http://www.embulk.org/docs/built-in.html bigquery output plugin : https://github.com/embulk/embulk-output-bigquery vertica input plugin : https://github.com/sonots/embulk-input-vertica https://medium.com/@jwlee98/embulk-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-oracle-db-%EC%97%90%EC%84%9C-bigquery-%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%82%BD%EC%A7%88%EA%B8%B0-141dc1d62b73 config 변수는 liquid template engine을 사용 : https://shopify.github.io/liquid/" } ]
