비즈엠의 카카오톡 알림톡 확실히 엄청 좋은 기능이다. 별 다른 약관 동의 없이도 목적이 인정되면 카카오톡으로 보낼 수 있다는 것이 얼마나 좋은가? 근데 그건 알바아니고 개발자인 우리는 귀찮을 뿐 비즈엠 카카오톡 알림톡 전송 API를 개발하면서 정리할만한 부분을 정리해봤다. 다른 개발자들은 나보다는 빨리 원하는 결과에 도달하기를 바라며 오늘도 글을 싸 본다. 뿌직뿌직
특이사항이라면 보통 템플릿을 지원하면 request에 템플릿 id와 교체해줄 문구만 보내면 알아서 발송해주는 개념으로 생각했는데 그게 아니라 템플릿의 모든 정보(모든 문자, 기타 설정값)를 다 보내줘야 한다. 이 부분은 이해가 안 된다. 굳이...? 글고 뭔가 내가 특정 유저라는 것을 증명하는 API 키 같은 것도 없긴 하다. 근데 Send API 호출 난이도가 좀 높은 편이라서 필요 없는 것도 이해는 간다.
2. 필요한 정보
알림톡 전송 API를 호출하기 위해 필요한 최소한의 정보다. 전부 찾아오세요
비즈엠 로그인 ID
비즈엠 로그인할때 쓰는 로그인 ID
비즈엠 발신프로필키
발신 프로필 > 발신 프로필 목록 > 발신프로필키 전송하고자 하는 메시지의 발신프로필키 값과 메시지의 생성한 계정이 발신프로필이 동일해야 한다.
템플릿 ID
템플릿 > 템플릿 목록 > 보내고자 하는 메시지의 템플릿코드
메시지
저 메시지 내용을 1byte도 안 틀리고 똑같이 갖고 있어야 한다.
실제 받는 사람 전화번호
이건 그냥 본인 테스트용 전화번호로 해보자
버튼 정보
템플릿에 등록한 버튼 정보 전부 다 갖고 있어야 한다.
뭔 발송하는데 벌써부터 이렇게 복잡하냐... 일단 전부다 모아 왔으면 코드로 설명하기 전에 간단히 개념적으로 설명 들어가겠다.
(node:498071) UnhandledPromiseRejectionWarning: TypeError [ERR_INVALID_CALLBACK]: Callback must be a function at makeCallback (fs.js:136:11) at Object.rmdir (fs.js:671:14) at router.get (/ServerCrwal.js:499:16) at Layer.handle [as handle_request] (/node_modules/express/lib/router/layer.js:95:5) at next (/node_modules/express/lib/router/route.js:137:13) at Route.dispatch (/node_modules/express/lib/router/route.js:112:3) at Layer.handle [as handle_request] (/node_modules/express/lib/router/layer.js:95:5) at /node_modules/express/lib/router/index.js:281:22 at Function.process_params (/node_modules/express/lib/router/index.js:335:12) at next (/node_modules/express/lib/router/index.js:275:10) (node:498071) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1) (node:498071) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
2. 원인
버전에 맞지 않는 구문을 사용해서 그렇다.
rmdir을 recursive 옵션 없이 사용하면 잘 작동하는데 문제가 발생한 환경의 node 버전은 10.x 버전이었다. 그리고 인터넷에 올라와있는 예시는 거의 12버전 이상에서 작동하는 코드들이다.
왼쪽이 10.x 구문, 오른쪽이 12.x 구문이다.
문제가 발생했다면 해당 환경해서 node -v로 버전을 확인해보고 12버전이상인지 확인해보자.
filesystem파일에 대하여 관리할 수 있도록 도와주는 라이브러리 이미 fs로 쓰고 있을 가능성이 높음.
마찬가지로 코드로 넘어가기 전에 간단히 어떤 플로우를 통하여 업로드 하는지 알아보자.
업로드보다 조금 더 쉽다.
1. axios에서 원하는 파일 데이터를 stream 형태로 요청
2. 결과값을 writestream으로 저장
3. 코드
//1. axios에서 원하는 파일 데이터를 stream 형태로 요청
AxiosNetworkHelper.GetRequest({
url: 'https://targetsite.com/image.jpg',
responseType: 'stream',
}).then((res) => {
//폴더 recursive로 생성 요청
if (!fileSystem.existsSync(`./images/savefolder`))
fileSystem.mkdirSync(`./images/savefolder`,{recursive: true})
//파일 이름 확장자명 따라서 정해주기
let fileName = `filename.${res.headers['content-type'].split('/')[1]}`
//2. 결과값을 writestream으로 저장
const writer = fileSystem.createWriteStream(`./images/savefolder/${fileName }`);
res.pipe(writer)
writer.on('error', (error) => {console.log(`이미지 다운로드 스트림 생성 실패 ${error}`)})
writer.on('close', () => {console.log('이미지 다운로드 성공')})
}).catch(error => {LogManager.getInstance().AddLocalLogData(forkHistoryId, `이미지 다운로드 요청 실패 ${error}`)})
마찬가지로 내용이 쉽기 때문에 자세한 설명은 생략하도록 하겠다.
다만 파일 확장자를 따로 정해주는 부분은 이해가 안갈수도 있는데
해당 res.headers 부분을 찍어보면 아래와 같이 나온다.
확장자 정보는 url을 통해서 이미 알고 있는데 왜 다시 정보를 이런식을 받아오냐 묻는다면... 가끔 확장자 정보가 없는 경우가 있다. 그런 경우 위와 같이 확장자를 그때그때 받아와서 저장하는 것 이다.
Error: Cannot find module 'ejs' Require stack: - C:\Users\user\Desktop\workspace\node_modules\express\lib\view.js - C:\Users\user\Desktop\workspace\node_modules\express\lib\application.js - C:\Users\user\Desktop\workspace\node_modules\express\lib\express.js - C:\Users\user\Desktop\workspace\node_modules\express\index.js - C:\Users\user\Desktop\workspace\ForkCraneServer\app.js at Function.Module._resolveFilename (internal/modules/cjs/loader.js:980:15) at Function.Module._load (internal/modules/cjs/loader.js:862:27) at Module.require (internal/modules/cjs/loader.js:1042:19) at require (internal/modules/cjs/helpers.js:77:18) at new View (C:\Users\user\Desktop\workspace\node_modules\express\lib\view.js:81:14) at Function.render (C:\Users\user\Desktop\workspace\node_modules\express\lib\application.js:570:12) at ServerResponse.render (C:\Users\user\Desktop\workspace\node_modules\express\lib\response.js:1012:7) at C:\Users\user\Desktop\workspace\ForkCraneServer\app.js:39:9 at Layer.handle [as handle_request] (C:\Users\user\Desktop\workspace\node_modules\express\lib\router\layer.js:95:5) at next (C:\Users\user\Desktop\workspace\node_modules\express\lib\router\route.js:137:13)
수많은 글들이 노드의 메모리 누수에 대한 글들을 설명하고 해결방법, 대안을 포스팅했지만 정말 겉만 번지르르하고 도움이 안 됐다.
사실 좀 화가 났다. 왜냐면 나는 개발을 다 해놓고 앱을 돌려보니까 메모리 누수가 발생했는데 어디서 누수가 발생했는지 감도 안 잡히고 찾아보면 대부분의 게시물이 '메모리 누수는 스택과 힙 영역에서 머시기머시기 -> 정말 간단한 테스트 코드로 메모리 할당된 거 보여주면서 요런 식으로 메모리 누수가 발생하고 이렇게 하면 안 됩니다.~~' 이렇게 끝난다. 뭐 어따쓰라고 ㅡㅡ
그래서 직접 부딪혀보면서 메모리 누수를 찾고 해결해내었다. 결론부터 말하자면 개발을 엄청 잘한다고 메모리 누수를 피해 갈 수 있는 것이 아니다. 왜냐면 정말 많은 사람들이 사용하는 라이브러리에서도 엄청난 메모리 누수가 발생하였기 때문이다.
즉 당신은 메모리 누수가 발생하였으면 직접 덤프를 까보며 분석하고 해결하는 능력을 길러야 한다.
앞으로 포스팅할 글들은 실제 개발하다가 메모리 누수가 발생하였고 누수에 대한 확인, 해결방법을 제시하도록 하겠다.
꽤나 가치 있는 게시물이 될 것이라고 장담한다.
2. 메모리 누수란?
메모리 누수에 대한 간단한 설명을 하겠다.
당신은 온라인 게임을 만들었다. 한 명의 유저가 접속하면 유저에 대한 객체를 생성하는데 1MB가 필요하다.
축하한다. 당신이 만든 온라인 게임에 유저 100명이 들어왔다. 100MB의 메모리를 할당받았다.
안타깝게도 당신의 게임이 너무 빨리 질려버린 탓에 50명의 유저가 종료를 해버렸다.
그래서 50개의 유저 데이터를 파괴하고 50MB의 메모리를 다시 돌려받아야 한다.
GC는 이러한 과정 속에서 사용이 완료되고 다시 사용할 수 있도록 메모리를 수거하고 다시 사용 가능하도록 관리한다.
여기서 문제가 발생한다. 정체불명의 이유로 인해서 유저 50명이 접속을 종료해도 GC가 판단하기에 50명의 데이터를 담 고 있던 메모리가 아직 사용이 완료됐다고 판단하지 않아서 메모리를 수거하지 않고 계속 남아있는 것이다.
이것이 계속 반복되면 결국 앱은 무한대로 메모리를 점유하게 될 것이고 언젠가 앱이 강제로 종료될 것이다.
메모리 할당 -> 사용 끝 -> GC가 수거 못함 -> 아무도 사용 안 하지만 계속 할당된 채로 남아있음 -> 반복 -> 서버 폭발
3. 메모리 누수 확인
그러면 실제 개발하다가 메모리 누수가 발생하면 어떻게 되는지 확인해보자
일단 먼저 본인의 앱에서 메모리 누수가 발생한다는 것을 인지해야 한다.
왜냐하면 당신의 앱에서도 충분히 메모리 누수가 발생하고 있지만 그 정도가
나는 메모리 누수에 대한 개념이 부족한 때라서 메모리 누수를 인지하는데 상당히 오래 걸렸다.
작업 관리자에서 Node가 차지하는 메모리를 보면 2000mb인 것을 볼 수 있다. 말도 안 되는 상황이지 않는가?
4. 노드 크롬 디버거
먼저 노드의 메모리 누수를 확인하기 위해서는 노드 크롬 디버거를 사용해야 한다.
노드 크롬 디버거를 사용하는 방법은 다음과 같다.
node --inspect 시작 파일
그러면 위와 같은 메시지가 추가로 나온다. 포트가 9229여야 하는데 꼭 9229일 필요는 없지만 구글 크롬에서 기본값으로 부착되는 포트가 9229라서 편하다
구글 크롬으로 들어간다.
URL에 chrome://inspect로 접근한다.
위와 같이 하단에 구동 중인 서버가 보인다. inspect를 눌러서 디버거를 켠다.
Memory > Profiles의 메모리 부분을 보면 현재 애플리케이션이 사용 중인 메모리를 실시간으로 볼 수 있다.
저 상태에서 메모리 부하를 일으키는 작업을 실행해보자 메모리가 하늘 높은 줄 모르고 계속 치솟는다.
그러다가 앱이 멈추면서 특정 부분에서 break를 한다. 우측을 보면 Paused before potential out-of-memory crash라고 나와있다. 메모리 부하가 심해서 임종 직전에 멈춘 것이라고 한다.
5. 누수 발생 확인
메모리 누수가 발생하는 원인을 확인해보는 여러 가지 방법을 설명하도록 하겠다.
메모리 스냅샷을 이용하여 부하를 일으키는 메모리는 어떤 종류인지 확인해보겠다.
메모리 부하를 일으킨 다음에 메모리 스냅샷을 떠서 용량이 많이 차지하는 덩어리를 한번 까보는 것이다.
일단 아무것도 하지 않은 위 상태에서 스냅샷을 한번 찍는다.
이전 단계와 마찬가지로 부하를 일으켜보고 어느 정도 됐다 싶으면스냅샷을 한번 더 찍는다.
두 개의 스냅샷이 저장됐다.
각 스냅샷의 Statics를 확인해보자.
좌측은 정상적인 어플리케이션 메모리의 분포 상황이고 우측은 딱봐도 이상하리만큼 Strings로 꽉찬 메모리 분포 상황이다. 이정도면 엄청나게 많은 string을 만들어놓고 gc 처리를 못해서 메모리 부하가 생긴다고 킹리적 갓심이 가능한 상황이다. ㅇㅈ?ㅇㅈ
이번에는 저 많은 string이 어떤 부분에서 발생하는지 확인해보도록 하자.
앱을 다시 시작하고 부하를 주지 않은 상태에서 Allocation sampling 을 선택하고 Start를 누르고 어느정도 부하가 되면 Stop을 눌러서 결과를 살펴보자.
Allocation sampling은 메모리 부하를 주는 자바스크립트의 메쏘드를 살펴보는 기능이다.
Tree형식으로 확인해보자
혼자서 무려 메모리 사이즈의 99.99%를 차지하는 불순한 녀석이 보인다. 저 녀석의 하위 트리를 따라내려가보면서 쭈욱 살펴보도록 하자
jsonparse라는 내가 설치한 라이브러리가 해당 문제를 일으키고 있으며 해당 라이브러리의 문제를 일으키는 메쏘드들을 확인할 수 있다.
그러면 마지막으로 메모리 누수가 발생하는데 어떤 형태의 데이터가 쌓이는지 그 값들을 직접 확인해보자
다시한번더 어플리케이션의 초기상태, 부하상태의 스냅샷을 각각 찍어보자
All Obejcts를 두 스냅샷 사이이로 해주고 Summary를 Comparison으로 해주자
그러면 두 스냅샷 사이에서 생성된 데이터를 볼 수 있다.
Alloc.Size로 내림차순 정렬을 하고 상단의 트리를 열어보자
그러면 어 저거 내가 생성한 데이터인데 왜 저기있지? 싶은것이 있으면 원인을 찾은 것이다.
분명 저 데이터는 처리되고 말소되어서 GC가 수거해가야하는데 수거하지 못하게끔 어딘가에 계속 맞물려있는 것이다.