0%

Let’sEncrypt?

이번에 서비스를 준비하면서 EC2에서 하나씩 올려서 해보기로 했다.
사실 AWS 기반이라 로드벨런서에 인증서 물리면 되는데…
전에 해보던게 생각 나기도 했고..
일단 서비스 규모가 더 커지면 그 때 안정적으로 가고 지금은 빌드업 수준으로 해보기로 했다.

일단 뭐 Let’sEncrypt에 대한 자세한 것은 이미 알고 있다는 가정 하에 바로 설치법부터 들어간다.

그 전에 환경은..

AWS 기반 EC2 Ubuntu 20.04 LTS 기준으로 작업을 진행
Nginx 사용 <- 중요하진 않음

Part 1 설치 커맨드

아래의 커맨드로 하나씩 진행하였다

1
2
3
4
5
6
sudo apt update
sudo snap install core; sudo snap refresh core
sudo apt-get remove certbot
sudo snap install --classic certbot # 설치
sudo ln -s /snap/bin/certbot /usr/bin/certbot # 링크
sudo certbot certonly --email [인증에 사용할 이메일] --standalone -d [적용할 도메인] 실행

커맨드 설명은 아래와 같다

1번 : 설치 전 업데이트 처리
2번 : 필요한 것들 설치
3번 : 기존에 설치된 certbot이 있다면 제거
4번 : certbot 설치
5번 : 심볼릭 링크 생성
6번 인증서 만드는데 email은 인증에 대한 대표자 메일(필자는 주로 쓰는 메일 사용)을 적고 -d는 적용할 도메인을 적어준다 (elfinlas.io.xx 등)

6번 명령어 이후 아래와 같이 뜨면 정상적으로 된 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@ip$ sudo certbot certonly --email test@test.com --standalone -d dev.net
Saving debug log to /your/some/path/letsencrypt.log
Requesting a certificate for dev.net

Successfully received certificate.
Certificate is saved at: /your/some/path/letsencrypt/live/dev.net/fullchain.pem
Key is saved at: /your/some/path/letsencrypt/live/dev.net/privkey.pem
This certificate expires on 9999-99-99. <= 여긴 인증기간은 3개월인가 줘서 사람마다 틀림
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

커맨드에 대해 좀더 설명이 필요한 분은 이곳 에서 직접 확인해보자

Part 2 AWS 작업

이렇게 진행하고 나서 AWS 콘솔로 가준다
콘솔에서는 다음과 같은 절차로 진행한다

  1. AWS 콘솔에서 Certificate Manager 이동
  2. 인증서 선택에서 각 항목에 해당 하는 값을 가져온다. (아까 생성한 pem 파일을 사용)
    1. 인증서 본문 => cert.pem
    2. 인증서 프라이빗 => privkey.pem
    3. 인증서 체인 => fullchain.pem
  3. 마무리 하면 ec2에 접속해서 콘솔에서 아랴와 같이 nginx를 재시작 해준다.
    1. sudo service nginx restart

아마 다른 블로그에서는 설치법만 나오고 보통 AWS 설명도 약간 부실했는데 직접 찾아보고 적용한 방법이다.
그리고 기반은 nginx 인데 아파치 쓰거나 다른 미들웨어 쓰시는 분들은 똑같이 서비스만 재시작 하면 된다.

인증서

처음 node를 하려다가…

이번에 진행하는 프로젝트에 노드를 적용해보려고 노드 공부를 시작했다.
노드 환경을 구축하고, npm은 문제가 많아서 페이스북에서 만든 yarn이라는걸 많이 쓰는 추세라 하여…
yarn을 사용하기로 마음먹고 실행해봤다.

그런데 역시 처음엔 고통과 배움의 시간이 필요하듯…
이상한 문제가 나에게 찾아왔다.

프로젝트를 지웠다가 다시 해도 계속 발생하고, 검색을 해도 잘 안나오거나 뭐라고 하는지 이해가 잘 안되었다.
지금 생각해보면 너무나도 간단한 문제였지만 -_-;;

내가 찾은 솔루션은 이 문서 였다.
내용을 요약하면…

너가 지금 하려는 디렉토리의 상위쪽에 package.json 또는 yarn.lock 파일이 존재한다

맨 처음엔 ‘상위에 저런게 있을리가 없지’ 라면서 그냥 넘겼는데..
도저히 못찾아서 헤매다가…

HEXO

그렇다.
이 블로그는 hexo로 만들어졌고, hexo는 node.js로 만들어졌는데, 이 hexo를 깔면서 홈 디렉토리에 뭔가를 했던 기억이 생각났다…
그리고 확인 결과….역시 있었다. (package-lock.json package.json)

그리고 두 파일을 제거하고 yarn 명령어를 사용했더니 잘 된다 -_-;

결론은 항상 안되면 솔루션의 글을 잘 읽고 확인도 잘 해보자.

iTerm2 에서 경로가 깨저서 나오는 경우

위와 같이 깨질 경우 설정에서 폰트를 바꿔주면 된다.

Preferences(command + ,) -> Profiles 탭 -> Text 항목 -> Font를 변경

위와 같이 변경한 경우 깨지는것은 사라진다.

잡담 전…

흐음…
인터넷 공간에 이렇게 잡담 형식 글을 올려보는건 처음이다.
개발과는 무관한 이야기여서 관심 없으시거나 바쁜 분들은 뒤로가기 ㄱㄱ


내가 지금까지 블로그를 하게 된 이유?

내가 블로그를 시작하게 된 것은 배운것을 정리하며, 남들이 내 글을 읽고 작은 도움이 되었음 해서였다.
난 시행착오를 그렇게 좋아하지 않으며, 명확한 것을 좋아한다.
개발과 관련되어 막히는 부분이 있어서 좀 찾아보면 다들 예제를 되풀이 하거나 실질적으로 도움이 그리 크지 않은 이론적인 부분 + 누구나 쉽게 접근할 수 있는 예제로만 구성이 되어 있었다.
물론 자신이 처한 상황과 코드 공개가 어려운 부분도 있을거고 다양한 이유가 있었을 것이다.
그런데 난 이런게 너무 싫었다.
그래서 내 블로그를 보면 최대한 내가 했던 경험을 토대로 보안적인 부분을 뺀 나머지를 다 공개했다.
물론 내가 올린 방법이 베스트는 아니겠지만…그래도 기본 예제를 벗어나 응용된 예제를 작성하는데 촛점을 뒀다.
나도 가끔 과거의 나에게 한수 배우고(?) 다시 돌아보며 정리를 어느정도 잘한 것이라고 생각이 든다.

근래에 개발과 관련된 것을 내려놓으며…

일단 내가 마지막 재직한 회사는 작은 스타트업이었다.
대기업에 잘 입사해서 다니다가 개발을 못한다는 이유 하나로 퇴사하여 이곳 저곳을 굴러다니면서…
사실 정말 많이 꼬였다. (물론 핑계다)
대기업에서 퇴사 전에 좀 더 신중에 신중을 가하고, 본인의 실력과 위치를 판단했어야 하는데 너무나도 오만하고, 자신을 과대평가 하였다.
뭐 그 결과 네이버나 카카오같은 당시 1등 2등 IT 기업으로 이직을 실패했다.
이유는 너무나도 당연하다.
기본기도 부족하고, 어정쩡한 실력으로 가려 했으니 말이다.
그런 부분을 보완하고자 스타트업으로 들어가서 실력을 키워보려 노력했다.
그런데 들어간 곳이 서비스 회사가 아닌 다른 성향의 회사라서 실력을 키웠다기 보단 잡스킬만 올린…1년의 시간을 버리게 되었다.

그 다음도 스타트업이었다.
물론 이번엔 블록체인으로 옮겼다.
그런데 이곳은 주 언어가 파이썬과 장고였고 다시 학습을 하면서 적응을 하며 노력했지만…
사실 네이버나 카카오를 포기하기 어려웠고, 자바로 쌓은 약 3년치 경력 (생각해보면 물경력...)이 아까워서 틈틈히 자바 개발을 하게 되었다.
그러다가 경력이 너무 꼬이는것 같아서 (사실 이미 많이 꼬였다.) 다시한번 마지막 이직을 하게 되었다.

여기는 이미 사라진 기업이라 뭐 공개해도 되겠지?
모빌이라는 회사였다.
지금은 인수합병 되어 사라진 회사다.
여기서는 참 많은 것을 배우기도 했지만, 회사가 스타트업이다보니 창업자 3명이 마음대로(사실 대표 한명이 엉망이었고, 기분에 따라 행동하는 사람이었다) 운영이 되었다.
특히 일정 부분이 정말 답이 없이 사람을 갈아 넣는 일정이었다.
난 그래서 환멸을 느끼고 퇴사를 하게 되었다.

퇴사를 하고 네이버 웹툰이나 카카오페이, NHN 등 다양한 회사에 면접을 보러 다녔지만 1차를 붙거나 아님 다 1차에서 떨어졌다.
면접 피드백을 받지 못했지만, 내 개인적인 생각엔 물경력 + 연차 대비 부족한 실력 + 부족한 기본기 라는 결론을 내렸다.
그래서 사실 집에서 쉬면서 내가 뭘 해야 할지 많은 고민을 했다.
말이 거창하지 그냥 백수가 되었다.
이렇게 되면서 사실상 개발의 배움을 포기하게 되었고 자연스래 기술 블로그도 내려놓게 되었다.

다른 직업군 전환 고민

내 나이를 딱 밝힐 수 없지만…
IT쪽에서는 이제 신입으로는 거부할 나이라는 것은 확실하다.
마지막 재직년도가 19년이었으니 이제 거의 2년이 되어간다.
이렇게 개발을 내려놓은지 2년동안 난 내 밥벌어 먹고 살 길을 다시 고민하게 되었다. (사실 아직도 못찾았다)

주식?

사실 벌어둔 돈으로 주식을 조금씩 해봤는데 공부를 안하고 바로 투자했다.

지금 생각하면 무식하고 멍청하며 돈을 버린 행위라 생각이 든다.
퇴사하고 좀 지나서? 코로나가 시작되고 유행이 피크를 찍을때였다.
삼성전자 주가가 5만원선 근처에서 난 버려도 되는 돈인 500만원 정도로 주식을 제대로 시작하게 되었다.
그리고 뭐 다들 알다시피 회복을 조금씩 하면서 내가 투자한 네이버,삼전,애플 등의 주식도 많이 올랐다.
주식을 몰랐지만 난 주변에서 주식하는 분들에게 어깨넘어 배운 지식으로 나만의 매수, 매도 규칙을 만들고, 덕분에 현금 비중과 주식 비중 모두 증가하게 되었다.

물론 해피엔딩은 아니다.
중간에 손절도 많이 했고, 지금은 LG전자와 삼전에 크게 물려서 파란맛 보고 있다.

포토그래퍼

마지막 직장을 다니면서 아주 좋은 분을 알게 되었고, 이 분 덕분에 카메라라는 것에 눈을 뜨게 되었다.

사실 난 카메라는 필요 없다라는 생각을 했던 사람이지만…
이게 폰으로 찍은건 컴퓨터로 오는 순간 이게 사진인지 그림판 픽셀로 찍은건지 결과물이 엉망이었다.

그래서 이런것을 그 지인에게 묻다가 결국 어느새 카메라를 구입하게 되었다.
물론 그 지인이 소니 a6000에 랜즈 3개를 좋은 값으로…(여담인데 이번에 또 칼짜이즈를 좋은 가격으로…)
처음에 카메라 받고 이곳 저곳 사진을 많이 찍으며 포토그래퍼로 살아볼까 했다.
그런데 찾아보니 여기도 참 만만치 않은 곳이며, 일단 감각적인 부분이 많이 필요했다.

그래서 이것은 취미로 남겨두기로 하고 다른 밥벌이를 찾아보기로 했다.
혹시 내가 찍은 사진이 궁금하신 분들은 (물론 홍보용으로…걸어본다…) 아래 인스타도 걸어두었으니 사진 보고 가고 싶은 분들은 가서 구경 ㄱㄱ
보시면 아시겠지만 취미 생활 수준이구나 라는것을 바로 알게 될듯…

사진 인스타그램

유튜버

난 내가 잘하는게 잡담? 노가리인거 같다.

아무리 생각해봐도 예전에 회사 다닐때나 대학생 때도, 연구시설 다닐 때도 입담이 좋아서 다들 재미있어 했다.
물론 남자 한정이었다 (군대 이야기X, 음담패설X)
남자 개그 코드가 좀 맞는것 같다.
근데 뭔가 어떤 주제로 해야 할지 그런걸 몰랐다.

그러다가 고프로를 접하게 되었고 타임랩스에 관심이 많아지게 되었다.
그래서 살던 곳이 뷰가 좋아서 타임랩스로 찍은 것을 모아서 한번 올려봤다.
다들 유튜브 시작해본 분들은 알겠지만 처음 시작에 많은 사람들이 볼 줄 알고 올리지만 사실 아무도 안본다 ㅎㅎ
나 또한 큰 기대(?)를 하며 직접 프리미어 프로로 편집하며 올렸지만 조회수가 2개월이 되어도 20회 미만이었다.
그래서 1차로 접었다… 약 2개월 좀 넘을 무렵?

난 차에 큰 관심이 있다. (왜 갑자기 차 이야기 나온지는 아래에…)
특히 슈퍼카… (대부분의 남자들이 좋아할듯?)
그래서 도산대로 가서 사진도 몇번 찍고 고프로로 찍고 다녔다(물론 결과물이 안좋아서…)
특히 노량진개미님처럼 되고 싶었는데 이것도 쉽지 않더라…

근데 올해 21년 아우디 드라이빙 익스피리언스 행사에 참석하면서 장비들을 챙겨들고 혼자서 촬영하고 왔다.
그런데 다녀온 후 일이 있었어서 건들지 못하다가 8월 중순 쯤 이것을 그냥 개인 브이로그 식으로 만들어서 개인용으로 쓰자 라는 마인드로 편집하였다.
그리고 버려뒀던 유튜브를 다시 손보고 영상을 업로드 하게 되었다.

요새 인스타 릴스가 떠서 유튜브도 숏츠라는게 생겼는데 이거 덕분에 홍보가 조금(?) 되었다.
물론 이 글을 쓰는 시점에 구독자는 7명이다 ㅎㅎ
뭐 근데 이번 유튜브는 돈을 바라보고 직업을 바라보고 한다기 보단 그냥 취미 생활로 해보자 라는 마인드로 시작하게 되었다.

잘되면 유튜버로 가고, 안되면 그냥 취미로?

혹시 21년 아우디 드라이빙 익스피리언스 가 궁금하신 분들을 위해..
또 한번 작은 홍보를 해봅니ㄷ,,,
거 들어가서 한번 보시고 구독 좋아요 눌러주시면 너무나도 감사하겠지만…강요를 싫어하기에 정말 잘 보고 재미있었다면 부탁을 드려봅니다 ㅎㅎ

TeamNoNo 유튜브 채널

PS : 요새 디2가 넘 재미있어서 게임을 또 파는…무분별 컨셉…

소규모 창업 (?)

사실 나만의 작은 회사를 만들어보고 싶었다.

직원수는 그냥 나 혼자여도 되고?
그래서 예전 18년도? 그때부터 머리에 담고 있던 서비스를 하나 구상하고, 조금씩 기획하면서 시작하였다.
개발을 놨다고 했지만…
송충이가 솔잎을 먹어야지…다른거 먹으면 탈나더라…

그래서 요새도 개발중이다.
커뮤니티인데 오픈베타 중이긴 한데 아직 홍보는 안했다.
요새 모바일 지원 필수인데…Semantic ui 로 했더니….
혼자서 다시 iOS, Android 개발도 해야 할 듯 싶다.

무튼 전직 개발자 직군을 살려서 서비스를 만들어보고 있다.
아주 천천히…그냥 압박 받지 말고 해보자 식으로..

곧 정식 출시하면 이곳에도 공개할 예정이며, 어떻게 개발했는지도 기록을 남기려 한다.
사실 노션에 다 정리하고 있는데 그걸 또 가공 해서 올릴 생각을 하니 머리가 어질어질 하다..

그래서 지금은?

사실 전에 카메라 지인이 작은 스타트업을 구상 단계라서 같이 해볼 생각이 있냐 하여 발 담그기로 했다.

근데 여기는 기술 스택이 node 였다.
요새 아주 핫한 노드…
그리고 타입스크립트를 해두면 리엑트 네이티브, 리엑트, 노드 다 가능해서 나도 사실 흥미가 좀 많이 생겼다.
그래서 몇일 전부터 노드 환경도 셋팅해서 이제 공부를 막 하려 한다.

지금까지 정리해서…내가 했던..그리고 지금 하고 있는거는…

  1. 주식
    • 그냥 하루 벌어 하루 먹고 사는 느낌으로 하는 중.
    • 자산 불리기는 멈추고 타임스톤으로 시간 뒤로가기 중
  2. 포토그래퍼(접음)
    • 깔끔하게?
  3. 유튜버
    • 그냥 새로운 취미생활이 되어 버림..
  4. 창업
    • 아주 조금이나마 제대로 해보려고 노력하는 분야

회고의 마지막…

이렇게 정리하니 뭔가 마음도 후련해 진것 같다.
사실 내 꿈은 적당한 오피스텔 하나 잡고 박스터나 m3, amg 하나 사서 카라이프 즐기는게 꿈이다.

더 나아가서 이럴일은 없겠지만 내 코인이 개떡상하고, 주식도 To-The-Moon 을 하면?
난 바로 우라칸 에보 스파이더나 488 피스타 계약하러 가야지 뭐…

하지만 꿈은 꿈일 뿐…너무 꿈만 꾸면 몽상가가 되고…몽상가의 결말은 무저갱으로 떨어지는 나락 인생 이라는 것을 잘 인지하고 있기에…
그냥 지금 하는거나 하면서 재미있게 사는게 나의 목표이지 않을까 싶다.

사실 개발자로 다시 잘 해볼까 싶었지만…
개발자로는 돈을 많이 벌기도 힘들 뿐더러 개발이라는 분야는 많은 시간을 요구하며, 많은 학습량을 요구한다. (물론 타분야도 그렇겠지만…)
또한 네카라쿠 가서 연봉 1억이네 뭐내 해도 사실 들여다보면 거의 회사에 매여서 사는 그런 인생이기에…
난 작은 노동으로 중간의 결과를 얻는…
요새 부동산으로도 작은 재미를 본 내가 느낀 것은…

인생은 어떤 레버리지를 땡기느냐에 따라 성공 또는 죽은자의 삶으로 나뉘더라

즉 노력 1해서 결과 1-2 얻으면 그건 좀 좋지 않은 결과다.
노력은 1했는데 결과가 한 3-4 정도 나와야 해볼만 한거 같다.

근데 내 견문이 부족해서 그럴거 같은데 내 한계적 시선에서 개발은 아무리 잘하고 뭔짓을 해도 노력 1 -> 결과 1~2 밖에 안나오는거 같다.
물론 천부적 재능있는 사람 빼고 평범인을 말하는 것이다.
이 글에 반박을 다는 사람도 있겠지만…
그건 뭐 시선의 차이니까…

뭐 정리하자면…난 당분간은 유유자적하며, 취미로 개발하는 삶을…일명 유사개발자 의 삶을 살아보려 한다.

앞으로 기술 블로그도 좀 손보고 해서 내가 그래도 개발자로 살았던 흔적을 정리하고..
지금 만드는 서비스에서 얻은 좋은 지식도 정리해서 공유하고…
이번에 새로 배우게 되는 노드 관련 지식도 쌓고 헤서..
이것도 하나의 좋은 컨텐츠가 되는게 이 블로그의 목표이다.

다른 멋진 기술 블로그에 비해 한없이 초라하고 부족하지만…
그래도 여기서 필요한 것을 얻게 되고 도움이 되셨으면 좋겠다는 생각을 정리하며…
엉망진창인 회고를 마무리 해본다.

방문 주신 분들에게 항상 좋은일이 있길 바라면서…
다른일이 많지만 디아2가 나를 부르는 소리가 들려서 이만….

RSA 키 충돌 문제 (REMOTE HOST IDENTIFICATION HAS CHANGED)

최근 인프라 쪽 작업을 하면서 ec2 에 작업을 좀 하다가 키를 바꾸게 되었다.
그리고 다시 접근을 하는데 아래와 같은 에러가 발생하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

shell > ssh -i "test.pem" 192.168.0.1

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Please contact your system administrator.
Add correct host key in [자신의 known_hosts 경로] to get rid of this message.
.....

이럴 경우 다시 키를 생성해서 처리해주면 된다.

ssh-keygen -R [접근할 주소]

1
2
3
4
5
shell > ssh-keygen -R [접근할 주소-ec2 주소 등]
...
[본인의 known_hosts 경로] updated.
Original contents retained as [본인의 known_hosts 경로]
...

위와 같은 출력이 뜬 뒤로 다시 해보면 잘 된다.

참고사이트

Aws ec2 ubuntu에서 타임라인 맞추기

두 가지 방법이 있다.

명령어 처리

아래의 명령어를 통해 처리한다

1
2
sudo rm /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

AWS 추천 방법 - Amazon Time Sync Service 구성 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Step 1 - 아래 커맨드 수행
sudo apt install chrony

# Step 2 - /etc/chrony/chrony.conf 파일 수정
vim /etc/chrony/chrony.conf

# Step 3 - 파일에 이미 존재하는 server 또는 pool 문 앞에 다음 라인을 추가하고 변경 사항을 저장
server 169.254.169.123 prefer iburst minpoll 4 maxpoll 4

# Step 4 - chrony 서비스 재시작
sudo /etc/init.d/chrony restart

# Step 5 - 확인
chronyc sources -v

참고링크

Java에서 Singleton 패턴이란?

Singleton(이하 싱글톤) 패턴은 자바에서 많이 사용한다.
먼저 싱글톤이란 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 객체를 만들어 사용하는 디자인 패턴= 을 의미한다.
즉 생성자의 호출이 반복적으로 이뤄져도 실제로 생성되는 객체는 최초 생성된 객체를 반환 해주는 것이다.
보통 아래와 같이 사용하게 된다.

1
2
3
4
5
6
7
8
9
10
11
public class ExampleClass {
//Instance
private static ExampleClass instance = new ExampleClass();

//private construct
private ExampleClass() {}

public static ExampleClass getInstance() {
return instance;
}
}

위 코드에서는 instance라는 전역 변수를 선언하는데 static을 줌으로써 인스턴스화 하지 않고 사용할 수 있게 하였지만 접근 제한자가 private 로 되어 있어 직접적인 접근은 불가능하다.
또한 생성자도 private으로 되어 있어 new 를 통한 객체 생성도 불가능하다.
결국 getInstance 메서드를 통해서 해당 인스턴스를 얻을 수 있게 된다.
위의 예제는 아주 작은 규모에서 사용할 수 있는 싱글톤 패턴이며, 다음에서 좀 더 알아보기로 한다.


그렇다면 싱글톤 패턴을 사용하는 이유는?

위에서도 언급된 바와 같이 한번의 객체 생성으로 재 사용이 가능하기 때문에 메모리 낭비를 방지할 수 있다.
또한 싱글톤으로 생성된 객체는 무조건 한번 생성으로 전역성을 띄기에 다른 객체와 공유가 용이하다.
이렇게만 보면 싱글턴이 좋아보일 수 있지만 문제점도 존재한다.


싱글톤의 문제점

싱글톤도 위에서 언급된 것 처럼 전역성을 띄면서 다른 객체와 공통으로 사용하는 경우와 같은 몇 가지 케이스에서만 사용할 때 효율적이며 그 외에는 문제점이 생길 수 있다.
일단 싱글톤으로 만든 객체의 역할이 간단한 것이 아닌 역할이 복잡한 경우라면 해당 싱글톤 객체를 사용하는 다른 객체간의 결함도가 높아져서 객체 지향 설계 원칙에 어긋나게 된다. (개방-폐쇄)
또한 해당 싱글톤 객체를 수정할 경우 싱글톤 객체를 사용하는 곳에서 사이드 이팩트 발생 확률이 생기게 되며, 멀티 쓰래드환경에서 동기화 처리 문제등이 생기게 된다.


다양한 싱글톤의 구현

싱글톤을 구현하는 방법은 몇 가지가 있는데 아래와 같이 구현할 수 있다.

static block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExampleClass {
//Instance
private static ExampleClass instance;

//private construct
private ExampleClass() {}

static {
try { instance = new ExampleClass();}
catch(Exception e) { throw new RuntimeException("Create instace fail. error msg = " + e.getMessage() ); }
}

public static ExampleClass getInstance() {
return instance;
}
}

위와 같이 static 블럭을 사용힐 경우 클래스가 로딩될 때 한번만 실행을 하게 되는 특성을 사용한다.
하지만 인스턴스가 사용되는 시점이 아닌 클래스 로딩 시점에 실행이 된다.

lazy init

위 static 방법에서 개선하여 클래스 로딩 시점이 아닌 인스턴스가 필요하여 요청할 때 생성되는 형태로 작성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
public class ExampleClass {
//Instance
private static ExampleClass instance;

//private construct
private ExampleClass() {}

public static ExampleClass getInstance() {
if (instance == null) { instance = new ExampleClass();}
return instance;
}
}

하지만 위 형태로 구성할 경우 멀티 쓰레드 환경에서 취약하다.
특정 쓰레드가 동시에 getInstance() 메서드를 호출하게 되면 인스턴스가 두 번 생성되는 문제가 발생한다.

Thread safe + lazy

1
2
3
4
5
6
7
8
9
10
11
12
public class ExampleClass {
//Instance
private static ExampleClass instance;

//private construct
private ExampleClass() {}

public static synchronized ExampleClass getInstance() {
if (instance == null) { instance = new ExampleClass();}
return instance;
}
}

Lazy에서 보였던 getInstance() 메서드에 synchronized 키워드를 붙임으로써 쓰레드에서 동시 접근에 대한 문제를 해결하였다.
하지만 synchronized 키워드는 성능 저하를 발생시킨다.

Holder

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExampleClass {

//private construct
private ExampleClass() {}

private static class InnerInstanceClazz() {
private static final ExampleClass instance = new ExampleClass();
}

public static ExampleClass getInstance() {
return InnerInstanceClazz.instance;
}
}

JVM의 클래스 로더 메커니즘과 클래스의 로드 시점을 이용하여 내부 클래스를 통해 생성 시킴으로써 쓰레드 간의 동기화 문제를 해결한다.
위 방법은 현재 java에서 싱글톤 생성에서 사용하는 대표적인 방법이다.


정리

싱글톤 패턴은 Spring framework에서도 많이 사용되며, 어떤식으로 구현하는지 알아두면 도움이 된다.
자바와 Spring에서의 싱글톤 차이점이라면, 싱글톤 객체의 생명주기가 다르다.
또한 자바에서 공유 범위는 Class loader 기준이지만, Spring에서는 ApplicationContext가 기준이 된다.


참고

What is PyCrawler

이번에 회사에서 Python을 이용하여 Daum Cafe를 크롤링하는 업무를 잠깐 진행하였다.
업무를 진행하면서 했던 내용을 정리할 겸 해서 github에 등록하였다.

자세한 코드는 PyCrawler Github에 등록하였다.

프로젝트를 간단하게 요약하면 특정 다음 카페의 게시판 정보, 게시판의 게시글 리스트, 본문을 크롤링 하고, 데이터를 가공하여 Django를 통해 Rest API 형식으로 제공하는 기능을 제공한다.
기타 참고사항은 아래와 같다.

  • Chrome Driver : Chrome/76.0.3809.100
  • Python 3.x
  • Django 2.2.4
  • BeatuifulSoup 4.8.0
  • Selenium 3.141.0
  • FBV 방식 사용

설명에 앞서 아직 기능이 많이 부족하다.
프로젝트는 완료되었지만…시간이 될 때마다 조금씩 업데이트를 해볼 수 있도록 노오려억을…


주요 기능

주요 기능은 크게 세 가지이며 아래에서 좀 더 자세하게 다루겠다.

게시판 리스트 정보 api

특정 카페의 게시판 리스트 정보를 가져오는 기능을 제공한다.
호출 시 아래와 같은 Json 정보를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"title": "카페 제목",
"board_cnt": "게시판 갯수",
"board_url": [
{
"name": "[공지사항]",
"url": "http://m.cafe.daum.net/...."
},
{
"name": "[기타사항]",
"url": "http://m.cafe.daum.net/..."
},
....
]
}

먼저 다음 카페의 ID를 알아야 한다.
다음 카페의 모바일 페이지로 이동을 한다. (모바일 사이트가 좀 더 크롤링하기 편하기 때문에…)

http://m.cafe.daum.net/

로그인을 한 다음에 카페 메인 페이지 화면에서, 각 브라우저의 소스보기 등으로 열어본다.
그리고 아래와 같은 부분을 찾아본다.

1
2
3
4
5
....

<a href="/[Daum Cafe ID]?nil=cafes" class="link_cafe #join_cafe_list">

....

위와 같이 class의 link_cafe 요소를 검색하면 알 수 있다.
찾은 Cafe id를 crawling.py 파일 상단의 Config 영역에 적어준다.
이 때 로그인할 계정과 암호도 입력해준다.

크롤링 코드의 경우 cafe_daum.py 코드의 get_board_list 함수를 참고한다.
워낙 간단하 코드라서 코드의 주석을 참고하면 파악하는데 큰 어려움은 없을 것이다.


특정 게시판의 게시글 리스트 조회 API

특정 게시판의 게시글 리스트를 가져오는 기능을 제공한다.
호출 시 아래와 같은 Json 정보를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"totalPage": "2",
"data": [
{
"name": "게시글 01",
"url": "http://m.cafe.daum.net/..."
},
{
"name": "게시글 02",
"url": "http://m.cafe.daum.net/..."
},
....
]
}

응답된 값에서 totalPage는 해당 게시판의 총 페이지 갯수를 의미하며, data에는 게시글 리스트와 게시글 url 값을 제공한다.

위 API를 호출하여 동작하는 함수는 cafe_daum.py 코드의 get_board_content_list 함수를 참고한다.
이 api를 호출할 때는 Request Header에 두 가지 값을 전달한다.

  • url-code
    • 이 값은 첫 번째 api에서 호출할 때 해당 게시판의 url을 넣어준다.
  • page
    • 게시판의 페이지 값을 입력한다.

해당 Header 값을 정상적으로 넣어줘야 파싱을 처리할 수 있다.


게시글 상세 조회 API

게시글의 상세 정보를 가져오는 기능을 제공한다.
호출 시 아래와 같은 Json 정보를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"content_body": "<p>안녕하세요</p> <p>게시글 테스트 입니다.</p>",
"attach_file": {
"attach_cnt": "2",
"attach_file": [
{
"name": "테스트01.JPG",
"url": "http://pd.cafe.daum.net/download.php?grpid=111&fldid=aas&dataid=11&fileid=1&disk=30&.JPG"
},
{
"name": "테스트02.JPG",
"url": "http://pds.cafe.daum.net/download.php?grpid=111&fldid=zzzz&dataid=11&fileid=2&disk=29&.JPG"
}
]
},
"write_user": "작성자",
"write_date": "06.04.18"
}

응답 Json의 형태는 아래와 같다.

  • content_body
    • 게시글 정보를 나타낸다.
    • 일반 태그는 다 제거하고 p, img, br 세 가지 태그만 추려서 가져온다.
  • attach_file
    • 첨부파일이 있는 경우 첨부파일을 담는 부분
      • attach_cnt
        • 첨부파일의 갯수
      • attach_file
        • 첨부파일의 이름과 다운로드 url을 담는다.
  • write_user
    • 작성자 닉네임
  • write_date
    • 작성 시각

위 API를 호출하여 동작하는 함수는 cafe_daum.py 코드의 get_board_content 함수를 참고한다.
이 api를 호출할 때는 Request Header에 아래의 값을 전달한다.ㄴ

  • url-code
    • 이 값은 두 번째 api에서 호출할 때 해당 게시판의 url을 넣어준다.

해당 Header 값을 정상적으로 넣어줘야 파싱을 처리할 수 있다.


정리

아직 기능이 많이 부족한 편이지만, 업무가 끝난 뒤에도 크롤링 관련 내용을 조금씩 추가해볼 예정이다.
이번 포스팅에서는 Daum cafe를 대상으로 하였지만, 다음에는 Naver cafe도 크롤링을 다뤄볼 예정이다.

Spring boot에 Telegram을 연동시 에러 발생

Spring boot에서 프로젝트 연동을 개발하였다.
그런데 개발 도중 운영 서버에 에러가 발생하였다는 알림이 있어서 확인해보니 아래와 같은 에러 로그가 무수히 쌓이고 있었다.

1
2
3
4
5
2019-08-14 17:00:54 ERROR :[BotLogger.java]severe(85) : BOTSESSION
org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException: Error getting updates
at org.telegram.telegrambots.meta.api.methods.updates.GetUpdates.deserializeResponse(GetUpdates.java:118)
at org.telegram.telegrambots.updatesreceivers.DefaultBotSession$ReaderThread.getUpdatesFromServer(DefaultBotSession.java:257)
at org.telegram.telegrambots.updatesreceivers.DefaultBotSession$ReaderThread.run(DefaultBotSession.java:186)

원인

이곳을 참고하여 문제 해결법을 찾게 되었다.
내가 개발한 환경은 하나의 봇에서 개발, 테스트, 운영 총 세 가지 환경에서 사용하고 있었다.
즉 각 환경에서 아래와 같은 코드가 중복적으로 들어가 있었다.

1
new TelegramBotsApi().registerBot(new MyAmazingBot());

즉 이미 등록된 봇 코드인데 중복해서 등록을 하여 발생한 문제이다.
이 문제를 해결하기 위해서는 registerBot 메서드를 실행할 때 한번만 등록 후 주석 처리 등을 해주면 된다.

사실 이 방법은 미봉책 방법인 것 같다.
이 부분에 대해서는 좀 더 찾아서 포스팅을 업데이트 해두도록 할 예정이다.

Spring Boot에서 Telegram 메세지를 보내기

개발하는 서비스의 업,다운 및 기타 특정 에러 발생 시 알림을 받는 것에 대해 고민하게 되었다.
메일의 경우 사실 바로 대응하기 힘들었고, 메세지형의 알림이 와야 바로 대응할 수 있을 것 같았다.
그래서 이번 포스팅에서는 텔레그램과 Spring boot와 연동하는 법에 대해서 포스팅 해보려 한다.

포스팅에 앞서 아래의 실행환경과 선행 조건이 충족되어 있다는 것을 가정하고 진행한다.

  • Spring boot 2.1
  • Lombok 사용
  • Gradle 사용
  • Telegram bot 생성 완료 참고
  • 참고한 TelegramBot의 경우 Github-TelegramBots을 참고
  • 예제코드는 Github 참고

1. Gradle 추가

먼저 프로젝트의 build.gradle 파일에 아래의 의존성을 추가한다.

1
implementation 'org.telegram:telegrambots:4.4.0.1'

2. Main Method 코드 추가

프로젝트의 Main Method에 아래의 코드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
//1
ApiContextInitializer.init();

//2
TelegramBotsApi botsApi = new TelegramBotsApi();
try {
botsApi.registerBot(new MyAmazingBot());
} catch (TelegramApiException e) {
e.printStackTrace();
}


SpringApplication.run(MovillContractApplication.class, args);
}

여기서 주의할 것은 1번, 2번의 작업이 run() 메서드 호출 전에 이뤄져야 한다.
주석의 1번 작업의 경우 Api context를 초기화 해주는 부분이다.
2번의 경우 봇 인스턴스를 등록하는 부분이다.
주의점으로는 각 봇의 경우 한번만 등록하여 호출할 수 있다.
registerBot 으로 하나의 봇을 등록할 수 있다는 의미이다.
필자의 경우 이 부분을 제대로 파악하지 못하여 개발용과 테스트, 그리고 운영 서버에 함께 등록을 하였더니 무수한 에러가 발생하였다.

이와 관련된 에러 발생 및 처리는 이곳을 참고하자.


3. 텔레그램 봇 클래스 생성

텔래그램의 봇 클래스를 하나 만들어준다.
생성 클래스는 아래 코드를 참고하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;

import javax.annotation.PostConstruct;


@Slf4j
@Component
public class TelegramMessageBot extends TelegramLongPollingBot { //
private final String BOT_NAME = "Input_Bot_Name"; //Bot Name
private final String AUTH_KEY = "Input_Bot_Auth-Key"; //Bot Auth-Key
private final String CHAT_ID = "Chat_ID"; //Chat ID

@Override
public String getBotUsername() {
return BOT_NAME;
}

@Override
public String getBotToken() {
return AUTH_KEY;
}

/**
* 메세지를 받으면 처리하는 로직
* @param update
*/
@Override
public void onUpdateReceived(Update update) {
//따로 처리하지는 않음
}

/**
* 메세지 전달
* @param sendMessage
*/
public void sendMessage(String sendMessage) {
SendMessage message = new SendMessage()
.setChatId(CHAT_ID)
.setText(sendMessage);
try {
execute(message);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
}

코드에 있는 주석을 참고하면 파악하는데 큰 어려움은 없겠지만, 몇 가지 보완 및 첨언을 통해 설명을 보강하겠다.

먼저 TelegramLongPollingBot 추상 클래스를 상속 받고, LongPollingBot 인터페이스에 선언된 메서드를 구현한다.
그리고 해당 클래스를 컴포넌트로 등록하여 빈으로 등록하여 사용한다.

getBotUsername 메서드에는 봇의 이름을 반환하게끔 처리하고, getBotToken 메서드에는 AuthKey를 반환하게끔 처리한다.
예제에서는 위와 같이 상수 값을 선언하고 처리하였는데, 그냥 메서드에서 반환해도 무방하다.

onUpdateReceived 메서드의 경우 해당 봇이 메세지를 받은 경우에 대해 처리하는 부분인데 이번 포스팅에서는 단순하게 메세지를 보내는 역할을 하는 부분까지 이므로 해당 메서드는 따로 처리하지 않았다.

sendMessage 메서드는 필자가 만든 커스텀 메서드인데 단순하게 메세지를 Chat ID에 해당하는 방으로 메세지를 전송하는 메서드이다.


4. 봇으로 메세지 보내기

봇으로 메세지를 보낼 때는 위에서 만든 빈을 이용하여 보낸다.
아래는 Boot가 시작 그리고 종료될 때 텔레그램으로 메세지를 보내는 샘플 코드이다.
실제 사용하는 코드에서 회사쪽 로직은 제거한 부분이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RefreshScope
@Slf4j
@Component
@RequiredArgsConstructor
public class StartUpApplicationListener {
//.... 변수 선언 부
private final TelegramMessageBot telegramMessageBot;


@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(event.getTimestamp()), TimeZone.getDefault().toZoneId());
String startMsg = "\n===== Application =====\n"
+ "=== SERVER START === \n\n"
+ "[Active Profile] : " + profile
+ "\n[Up-Time] : " + dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));

//특정 프로필일 때만 전송 처리
if (Arrays.asList("dev", "real", "real-slave", "stage", "stage-slave").contains(profile)) {
telegramMessageBot.sendMessage(startMsg);
}
}

@EventListener
public void shutdownApplicationEvent(ContextClosedEvent event) {
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(event.getTimestamp()), TimeZone.getDefault().toZoneId());
String startMsg = "\n===== Application =====\n"
+ "=== SERVER DOWN === \n\n"
+ "[Active Profile] : " + profile
+ "\n[Down-Time] : " + dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));

//특정 프로필일 때만 전송 처리
if (Arrays.asList("dev", "real", "real-slave", "stage", "stage-slave").contains(profile)) {
telegramMessageBot.sendMessage(startMsg);
telegramMessageBot.onClosing();
}
}
}

위와 같이 처리할 경우 아래의 사진과 같이 서버가 올라가거나 내려갈 때 텔레그램 봇으로 메세지가 오게 된다.


정리

이번 포스팅을 통해 간단하게 텔레그램 봇에게 메세지를 전송하는 부분을 알아보았다.
사실 자료를 찾아보다가 Java쪽 보다는 Node.js나 Python이 더 이런 쪽에 특화되어 있다는 것을 알게 되었다.
하지만 java로도 구현이 가능하며, 간단한 기능 등은 위와 같이 쉽게 연동할 수 있다.

다음 포스팅에서는 몇 가지 상태 등을 조회할 수 있는 기능에 대해 포스팅을 해보도록 하겠다.

PS : Github에 예제를 등록하여야 하는데… 주말에 시간이 될 때 등록해서 수정해야 겠다.
8월 26일 Github 등록완료