목차

반응형

증상

nodejs의 sequilize로 ORM 설정하고 생성된 테이블에 데이터를 넣은 뒤 findall로 데이터를 조회하려고 해도 아무것도 조회가 안된다.

 

원인

sequlize는 기본적으로 설정한 테이블 이름에 s를 붙여서 쿼리를 생성한다.

(정말 라이브러리 제작자 오지랖이 너무너무 넓어서 생긴 이슈다. 니들만쓰냐?)

 

sequlize 관련 함수 실행시 테이블 정보에 s가 붙는지 확인해보자.

또한 조회부터 하지 말고 삽입부터 해보고 생성된 테이블에 s가 붙었는지 확인해보자.

 

해결방법

sequlize ORM 정의할때 위와 같이 freezeTableName:true 옵션값에 true를 주도록 하자.

그러면 원하는 테이블 이름과 정확히 일치하는 쿼리가 생성된다.

 

추가사항

만약 본인이 이 문제로 들어왔다면 추가로 아래 문제도 발생할 가능성이 높으니까 이것까지 보고가라

 

자꾸 테이블에 id 필드가 자동으로 생성됩니다.

ORM 생성시 특정 필드에 아래 옵션값을 넣어주면 pk역할을 하는 id 필드가 안생긴다.

primaryKey: true로 해결

아래 링크 참고

https://stackoverflow.com/questions/29233896/sequelize-table-without-column-id

 

자꾸 테이블에 `createdAt`, `updatedAt` 컬럼이 알아서 생성됩니다.

ORM에 정의시 아래 옵션 넣어주기

timestamps: false 로 해결

https://newbedev.com/sequelize-unknown-column-createdat-in-field-list

 

진짜 역대급 오지랖이다.

왜 본인들의 스타일을 강요해서 무조건 에러를 만들어 버리는걸까...?

 

 

반응형
반응형

1. 개요

비즈엠의 카카오톡 알림톡 확실히 엄청 좋은 기능이다. 별 다른 약관 동의 없이도 목적이 인정되면 카카오톡으로 보낼 수 있다는 것이 얼마나 좋은가?
근데 그건 알바아니고 개발자인 우리는 귀찮을 뿐
비즈엠 카카오톡 알림톡 전송 API를 개발하면서 정리할만한 부분을 정리해봤다.
다른 개발자들은 나보다는 빨리 원하는 결과에 도달하기를 바라며 오늘도 글을 싸 본다. 뿌직뿌직

특이사항이라면 보통 템플릿을 지원하면 request에 템플릿 id와 교체해줄 문구만 보내면 알아서 발송해주는 개념으로 생각했는데 그게 아니라 템플릿의 모든 정보(모든 문자, 기타 설정값)를 다 보내줘야 한다.
이 부분은 이해가 안 된다. 굳이...?
글고 뭔가 내가 특정 유저라는 것을 증명하는 API 키 같은 것도 없긴 하다. 근데 Send API 호출 난이도가 좀 높은 편이라서 필요 없는 것도 이해는 간다.

 

2. 필요한 정보

알림톡 전송 API를 호출하기 위해 필요한 최소한의 정보다.
전부 찾아오세요

 

비즈엠 로그인 ID

비즈엠 로그인할때 쓰는 로그인 ID

 

비즈엠 발신프로필키

발신 프로필 > 발신 프로필 목록 > 발신프로필키
전송하고자 하는 메시지의 발신프로필키 값과 메시지의 생성한 계정이 발신프로필이 동일해야 한다.

 

템플릿 ID

템플릿 > 템플릿 목록 > 보내고자 하는 메시지의 템플릿코드

 

메시지

저 메시지 내용을 1byte도 안 틀리고 똑같이 갖고 있어야 한다.

 

실제 받는 사람 전화번호

이건 그냥 본인 테스트용 전화번호로 해보자

 

버튼 정보

템플릿에 등록한 버튼 정보 전부 다 갖고 있어야 한다.

 

뭔 발송하는데 벌써부터 이렇게 복잡하냐...
일단 전부다 모아 왔으면 코드로 설명하기 전에 간단히 개념적으로 설명 들어가겠다.

 

비즈 엠 카카오톡 알림 톡 Send API의 호출하기 위한 정보는 아래와 같다.

let options = {
method:"POST",
"url":`https://alimtalk-api.bizmsg.kr/v2/sender/send`,
headers: {
"userid": "loginid",
"Content-Type": "application/json",
},
data: [{
        "message_type":"at","profile":"23456422626","phn":phone,
        "msg":tempMsg,
        "tmplId":join,
        "button1":

{
            "name": "마켓쿵 바로가기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }


    }]
}

비즈엠 로그인 ID는 헤더의 userid
이후 발송 요청에 대한 내용은 request body로 들어간다.(기본적으로 배열)

 

3. 코드

const templateInfo = {
    "join": {
        msgParamCount: 3,
        msgBody: "내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo: {
            "name": "마켓쿵 바로가기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }
    },
    "payment":{msgParamCount:2, msgBody:"내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo:{}
    },
    "login": {
        msgParamCount: 6,
        msgBody: "내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo: {
            "name": "주문 내역 조회하기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }
    },
    "test": {
        msgParamCount: 5,
        msgBody: "내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo: {
            "name": "배송 조회하기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }
    }
}

exports.alrimtalkService = {
    sendAlrimtalkMessage(templateId, messageParams, phone) {
        return new Promise(async (resolve, reject) => {
            //1. 정보 전부 제대로 들어왔는지 확인
            if(!templateId || !messageParams || !phone)
            {
                console.log("[alimtalk] : sendKakaoTalkMessage 함수 호출시 인자 설정이 제대로 되지 않았습니다.")
                return resolve("[alimtalk] : sendKakaoTalkMessage 함수 호출시 인자 설정이 제대로 되지 않았습니다.")
            }

            //2. 템플릿 정보 있는지 확인
            if(!(templateId in templateInfo))
            {
                console.log(`[alimtalk] : 존재하지 않는 템플릿 정보 ${templateId}`)
                return resolve(`[alimtalk] : 존재하지 않는 템플릿 정보 ${templateId}`)
            }

            //3. 템플릿 정보의 메시지 인자 수와 함수 호출된 메시지 인자 개수가 같은지 확인
            if(templateInfo[templateId].msgParamCount !== messageParams.length)
            {
                console.log(`[alimtalk] : 메시지 인자 개수가 일치하지 않습니다. ${templateInfo[templateId].msgParamCount} : ${messageParams.length}`)
                return resolve(`[alimtalk] : 메시지 인자 개수가 일치하지 않습니다. ${templateInfo[templateId].msgParamCount} : ${messageParams.length}`)
            }

            //전송 바디 생성
            let body = buildRequestBody(templateId, messageParams, phone)

            let options = {
                method:"POST",
                "url":`https://alimtalk-api.bizmsg.kr/v2/sender/send`,
                headers: {
                    "userid": "633463466",
                    "Content-Type": "application/json",
                },
                data: body
            }

            //4. 전송 제대로 됐는지 확인
            return await axios(options).then(result =>{
                if(result.data[0].code !== "fail") resolve(result)
                else
                {
                    console.log(`[alimtalk] : ${result.data[0].message}`)
                    resolve(result.data[0].message)
                }
            }).catch(error => {
                console.log(`[alimtalk] : ${result.data[0].message}`)
                resolve(`[alimtalk] : 비즈엠 전송 실패 ${error}`)
            })
        });
    }
}


function buildRequestBody(templateId, messageParams, phone)
{
    let tempMsg = templateInfo[templateId].msgBody
    for(let i = 0;i < templateInfo[templateId].msgParamCount;i++)
        tempMsg = tempMsg.replace(/#{.+}/,`${messageParams[i]}`)

    return [{
        "message_type":"at","profile":"757545775753","phn":phone,
        "msg":tempMsg,
        "tmplId":templateId,
        "button1":templateInfo[templateId].buttonInfo
    }]
}

//실제 사용 코드
await alrimtalk.alrimtalkService.sendAlrimtalkMessage("test", [purchase.order.sender.username, purchase.productName, purchase.order.orderNo, purchase.order.receiver.address1, purchase.delivery.number], purchase.order.sender.phone)

코드의 buildRequestBody와 templateInfo에 대한 부분은 꼭 필요한 건 아니지만 최소한 이 정도 설계 없이는 개판일 것 같아서 설계한 거니까 적당히 이해해주세여

반응형
반응형

오류

axios를 한두번 요청하는 경우에는 문제없지만 몇천번의 요청을 하는 경우 한두번꼴로 request 이후 어떠한 결과값도 반환하지 않고 그대로 홀딩되버리는 경우가 발생한다.

당연히 timeout도 옵션값에 넣었는데 timeout 작동도 잘 하지만 그냥 그게 문제가 아니다. ㅇㅇ

정말 귀신이 곡할 노릇이라서 axios 라이브러리가 문제라고 생각하기도 쉽지 않았다.

여기까지 찾아 들어온 당신 꽤 하는거다.

보상으로 어렵게 얻어낸 해결 방법을 공유하도록 하겠다.

 

원인

원인은 잘 모르겠다. 아래에도 비슷한 상황이 있는데 흔한 케이스도 아니고 어쩌면 라이브러리 버전 문제일까 싶기도 한데 딱히 찾아봐도 뭐가 안나왔다.

https://github.com/axios/axios/issues/459

 

해결

해결방법은 axios의 timeout을 사용하지말고 자체적으로 타이머를 걸어서 cancel-token이라는 기능과 함께 해당 요청이 시간이 지나면 내가 직접 취소처리를 해버리는 것 이다.

nodejs axios cancel token 이라고 검색해서 찾아보면 사용 방법이 잘 나온다.

어떤 느낌인지 보고 싶으면 아래 내가 적용하기 전과 후 코드를 봐보자.

 

왼쪽이 before, 오른쪽이 after

좀 더러워지긴 했는데 이렇게 하니까 잘 된다.

반응형
반응형

1. 오류

nodejs 모듈 fileSystem의 rmdir 기능을 사용했다.

 

코드는 아래와 같다.

fileSystem.rmdir(`./images/${insertItem.option.forkHistoryId}/${insertItem.item.itemId}`, {recursive: true},()=>{})

 

그러면 아래와 같은 에러가 발생한다.

(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버전이상인지 확인해보자.

 

3. 해결방법

코드상의 문제는 없기 때문에 node 버전을 업그레이드 했다.

 

우분투 명령어

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -

sudo apt-get install -y nodejs

 

데비안 명령어

curl -sL https://deb.nodesource.com/setup_12.x | bash -

apt-get install -y nodejs

 

버전 업글하고 하면 잘된다.

반응형
반응형

1. 개요

2. 사용하는 라이브러리, 개념 설명

3. 코드

4. 그 외

 

 

 

1. 개요

이전 게시물에서는 파일을 업로드 하는 경우에 대해서 다뤘는데 이번에는 다운로드하여 저장하는 부분을 살펴보도록 하겠다.

마찬가지로 axios를 통해서 처리한다.

 

2. 사용하는 라이브러리, 개념 설명

사용하는 라이브러리

axios 네트워크 통신

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을 통해서 이미 알고 있는데 왜 다시 정보를 이런식을 받아오냐 묻는다면... 가끔 확장자 정보가 없는 경우가 있다. 그런 경우 위와 같이 확장자를 그때그때 받아와서 저장하는 것 이다.

 

4. 그 외

마찬가지로 현재 게시물의 예제는 단일 이미지 다운로드에 대한 예제다.

다음에는 여러개의 이미지를 동시에 다운로드 하는 예제를 올리려고 한다.

 

nodejs axios readstream 파일 업로드

nodejs axios writestream 파일 다운로드

 

반응형
반응형

1. 개요

2. 사용하는 라이브러리, 개념 설명

3. 코드

4. 그 외

 

 

 

1. 개요

노드 서버 백엔드상에서 파일을 주고 받는 경우가 있다.

그런 경우 axios나 request를 통해 주고받는데 생각보다 예제 코드가 많지 않아서 지금 사용중인 코드를 올리도록 하겠다.

reqeust로도 가능하지만 요청 실패시 catch 부분에 대한 설정을 못하는 것 같아서(찾아봐도 안나오더라) axios로 갈아탔다.

 

2. 사용하는 라이브러리, 개념 설명

사용하는 라이브러리

axios 네트워크 통신

form-data axios의 네트워크 요청 시 파일 업로드같이 특수한 형태의 요청을 설정하기 위해서 필요하다.

filesystem 파일에 대하여 관리할 수 있도록 도와주는 라이브러리 이미 fs로 쓰고 있을 가능성이 높음.

 

3가지 라이브러리를 설치하고 나면 파일 업로드에 대한 개발이 가능하다.

코드로 넘어가기 전에 간단히 어떤 플로우를 통하여 업로드 하는지 알아보자

 

1. formdata 변수 생성

2. formdata에 파일 stream 데이터 주입

3. axios에 생성해둔 formdata로 요청

 

3. 코드

//1. formdata 변수 생성
const formData = new FormData();

//2. formdata에 파일 stream 데이터 주입
formData.append("promisedParamName", fileSystem.createReadStream(`target.jpg`));

const requestConfig = {
    url: "https://mytargetsite.com/api/file/photoinfra/uploads",
    headers: {
        'Content-Type':'multipart/form-data; boundary=' + formData.getBoundary()
    },
    data: formData
}

//3. axios에 생성해둔 formdata로 요청
AxiosNetworkHelper.PostRequest(requestConfig).then(res =>{
    if(res.status === 200) console.log("성공")
    else console.log(`요청 실패 ${res.status}`)
}).catch(error => {console.log(`전송 실패 ${error}`)})

 

자세한 설명은 하지않겠다.

다만 저기서 formData.getBoundary()가 어떤 역할을 하는지 알려주겠다.

웹 request가 날아갈때 multipart의 경우 boundary라고 해서 데이터간의 구획을 나누는 역할을 해주는 표지판 역학을 하는 코드가 필요한데 그 정보를 axios의 요청에 넣어주는 것 이다.(이해 안가면 아래 웹 리퀘스트 참고)

Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
User-Agent: axios/0.21.1
Transfer-Encoding: chunked


Content-Type: multipart/form-data; boundary=--------------------------637277032015795830863476
content-length: 67576

 

4. 그 외

현재 게시물의 예제는 단일 이미지 업로드에 대한 예제다.

근데 살다보면 단일 이미지 업로드가 아닌 2개 이상의 이미지 업로드를 해야 할 필요가 있다.

다음은 그런 경우에 대한 예제 코드를 올리도록 하겠다.

 

nodejs axios readstream 파일 업로드

nodejs axios writestream 파일 다운로드

반응형
반응형

ejs를 설치했는데도 안되는 사람에게 해당되는 글이다.

 

정상적으로 npm install ejs를 했고

app.set('view engine', 'ejs')도 했는데

router단에서 res.render시 아래 오류가 발생한다.

 

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)

 

아래 오류를 자세히 보면 알겠지만 현재 내 프로젝트 경로가 제대로 지정되지 않았다.

따라서 프로젝트 경로를 지정해줘야한다.

 

아래 코드를 통해서 ejs 모듈의 참조 경로를 지정해주자.

app.engine('ejs', require('ejs').__express)

반응형
반응형

1. 개요

2. 메모리 누수란?

3. 메모리 누수 확인

4. 노드 크롬 디버거

5. 누수 발생 확인

 

 

 

 

수많은 글들이 노드의 메모리 누수에 대한 글들을 설명하고 해결방법, 대안을 포스팅했지만 정말 겉만 번지르르하고 도움이 안 됐다.

사실 좀 화가 났다. 왜냐면 나는 개발을 다 해놓고 앱을 돌려보니까 메모리 누수가 발생했는데 어디서 누수가 발생했는지 감도 안 잡히고 찾아보면 대부분의 게시물이 '메모리 누수는 스택과 힙 영역에서 머시기머시기 -> 정말 간단한 테스트 코드로 메모리 할당된 거 보여주면서 요런 식으로 메모리 누수가 발생하고 이렇게 하면 안 됩니다.~~' 이렇게 끝난다. 뭐 어따쓰라고 ㅡㅡ

그래서 직접 부딪혀보면서 메모리 누수를 찾고 해결해내었다. 결론부터 말하자면 개발을 엄청 잘한다고 메모리 누수를 피해 갈 수 있는 것이 아니다. 왜냐면 정말 많은 사람들이 사용하는 라이브러리에서도 엄청난 메모리 누수가 발생하였기 때문이다.

즉 당신은 메모리 누수가 발생하였으면 직접 덤프를 까보며 분석하고 해결하는 능력을 길러야 한다.

앞으로 포스팅할 글들은 실제 개발하다가 메모리 누수가 발생하였고 누수에 대한 확인, 해결방법을 제시하도록 하겠다.

꽤나 가치 있는 게시물이 될 것이라고 장담한다.

 

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가 수거해가야하는데 수거하지 못하게끔 어딘가에 계속 맞물려있는 것이다.

이렇게 해서 원인을 찾아봤고 다음은 해결하는 방법에 대해서 작성해보겠다.

 

 

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형
반응형

Nodejs의 기본 메모리 사이즈를 512MB 초과하면 자동으로 앱이 종료된다.

기본 메모리 사이즈를 늘리는 방법에 대해서 알아보자.

 

커맨드라인 환경에서는 node --max-old-space-size=12000 app.js 로 하면 된다.

숫자는 mb를 의미하는데 12000이면 대략 12GB의 메모리까지 허용하겠다는 의미다.

 

 

 

 

메모리를 늘려서 앱에 5.2GB의 메모리가 할당되어 있는 모습이다.

 

추가로 인텔리제이에서는 어떻게 설정하고 간단하게 실행하는지도 알아보자

 

우측 상단 Add Configuraiton

 

+ 클릭

 

Node.js 선택

 

우측과 같이 설정을 해주자.

NODE_OPTIONS=--max-old-space-size=1000

=뒤에 오는 숫자는 할당될 메모리다. 단위는 MB

 

 

만들어진 설정을 선택하고 시작하면된다.

 

 

Nodejs 메모리 관련 글

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형
반응형

1. 오류

2. 해결방법

 

 

 

1. 오류

노드를 하다 보면 아래와 같은 오류를 만날 수 있다.

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

<--- Last few GCs --->


[26828:0000020CB062E7D0]   706683 ms: Scavenge 7849.4 (8214.7) -> 7839.2 (8214.7) MB, 556.0 / 0.0 ms  (average mu = 0.131, current mu = 0.014) allocation failure 
Writing Node.js report to file: report.20200214.173258.26828.0.001.json[26828:0000020CB062E7D0]   708430 ms: Scavenge 7851.5 (8214.7) -> 7842.8 (8215.7) MB, 1247.6 / 0.0 ms  (average mu = 0.131, current mu = 0.014) allocation failure 
[26828:0000020CB062E7D0]   708535 ms: Scavenge 7855.6 (8215.7) -> 7846.4 (8218.7) MB, 32.1 / 0.0 ms  (average mu = 0.131, current mu = 0.014) allocation failure 


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 00007FF742BAB21D]
Security context: 0x0314ac3808d1 <JSObject>
    1: /* anonymous */ [000002D987624109] [C:\Users\Desktop\project\node_modules\jsonparse\jsonparse.js:~127] [pc=000001C121A8459D](this=0x0314ac3b5539 <Parser map = 0000007B595C11A9>,0x02a34c9a8251 <Uint8Array map = 00000236682E5379>)
    2: /* anonymous */ [00000314AC3B0549] [C:\Users\Desktop\project\node_modules\JSONStream\inde...


Node.js report completed
 1: 00007FF741FAF22F napi_wrap+125087
 2: 00007FF741F4FAA6 public: bool __cdecl v8::base::CPU::has_sse(void)const __ptr64+35302
 3: 00007FF741F50776 public: bool __cdecl v8::base::CPU::has_sse(void)const __ptr64+38582
 4: 00007FF742767D4E private: void __cdecl v8::Isolate::ReportExternalAllocationLimitReached(void) __ptr64+94
 5: 00007FF742750371 public: class v8::SharedArrayBuffer::Contents __cdecl v8::SharedArrayBuffer::Externalize(void) __ptr64+833
 6: 00007FF74261BDEC public: static void __cdecl v8::internal::Heap::EphemeronKeyWriteBarrierFromCode(unsigned __int64,unsigned __int64,class v8::internal::Isolate * __ptr64)+1436
 7: 00007FF742627040 public: void __cdecl v8::internal::Heap::ProtectUnprotectedMemoryChunks(void) __ptr64+1312
 8: 00007FF742623B64 public: static bool __cdecl v8::internal::Heap::PageFlagsAreConsistent(class v8::internal::HeapObject)+3204
 9: 00007FF742619373 public: bool __cdecl v8::internal::Heap::CollectGarbage(enum v8::internal::AllocationSpace,enum v8::internal::GarbageCollectionReason,enum v8::GCCallbackFlags) __ptr64+1283
10: 00007FF7426179E4 public: void __cdecl v8::internal::Heap::AddRetainedMap(class v8::internal::Handle<class v8::internal::Map>) __ptr64+2452
11: 00007FF742638C7D public: class v8::internal::Handle<class v8::internal::HeapObject> __cdecl v8::internal::Factory::NewFillerObject(int,bool,enum v8::internal::AllocationType,enum v8::internal::AllocationOrigin) __ptr64+61
12: 00007FF74239CF71 public: class v8::internal::interpreter::JumpTableTargetOffsets::iterator & __ptr64 __cdecl v8::internal::interpreter::JumpTableTargetOffsets::iterator::operator=(class v8::internal::interpreter::JumpTableTargetOffsets::iterator && __ptr64) __ptr64+1681
13: 00007FF742BAB21D public: virtual bool __cdecl v8::internal::SetupIsolateDelegate::SetupHeap(class v8::internal::Heap * __ptr64) __ptr64+544781
14: 000001C121A8459D 

Process finished with exit code 134

궁금할 것도 없이 과도한 메모리 점유로 인하여 js 엔진이 폭발해버린 것이다.

 

 

노드를 돌리고 있는데 노드의 메모리가 엄청나게 올라간다.

아니 도대체 뭘하길래 저렇게 올라가느냐...?

내가 하고 있는 작업은 10GB 정도 되는 json 데이터를 stream pipe로 가져와서 읽는 작업을 한다.

개발 초기단계에는 몰랐지만 두곳에서 엄청난 메모리 누수가 발생하고 있어서 데이터를 읽을수록 메모리 점유가 계속해서 올라가는 문제가 있었다.

 

노드의 기본 메모리 사이즈는 512MB다.

 

 

 

2. 해결방법

근본적인 생각해볼만한 것은 두 가지가 있다.

1. 더 큰 메모리 할당

2. 앱 메모리 누수 개선

 

1번의 경우 노드 옵션 설정이기 때문에 정말 간단하다.

정말 어려운 것은 2번이다. 노드가 어디에서 메모리 누수가 일어나는지 확인하는 것이 여간 쉽지 않고 어디 나와있지도 않은 부분이다.

앱 메모리 누수를 개선하는 방법에 초점을 맞춰 글을 써 나가도록 하겠다.

 

 

 

Nodejs 메모리 관련 글

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형