Android In-App Purchase Validation

얼마전 아랍어 번역을 마치고 몇개 국가에 앱을 출시하게 되면서 결제 관련 문제가 발생하기 시작했습니다. 앱과 서버의 결제 관련 코드를 모두 제가 맡아 작성했기 때문에 계속해서 발생하는 문제로 인해 정신적 충격이 상당했습니다…

“Android In-App Billing 보안 완벽 정리”의 글을 참고해 결국에는 앱내 구매(In-App Billing)시 프리덤(Freedom)과 같은 **결제 해킹 앱**에 의해 문제가 발생하게 된다는것을 알게 되었습니다.

이에 서버에서 **In-App Purchase Validation(앱내 구매 유효성 검사)**을 하는 코드를 추가했고 위의 문제를 해결할 수 있었습니다. 이번 포스팅에서는 In-App Purchase Validation에 대해서 알아보겠습니다. (In-App 결제 구현에 대한 내용은 다루지 않습니다!)

어떤 문제가 발생했는가?

어느날 이상한일이 벌어졌다. 분명 Admin을 통해 앱에서 결제한 내용을 확인했을때는 상당한 내역이 있었는데, Google Play Console의 주문 관리를 통해 확인했을 때는 결제 내역이 없는 것이다.

결제 관련된 부분에서 버그가 발생했기에 무척이나 마음이 심란했다. 반복되는 테스트에서도 재현할 수 없는 현상에 라이브 채팅을 통해 Google에 문의하였지만 돌아오는 답변은 사용자의 결제 관련 설정(예를들면 카드)이 잘못되어 있을 것이라는 말뿐, 정확한 해결 방법을 알려주지 않았다.

그러던 도중 결제 관련 DB를 살펴보다가 이상한 부분을 발견했다.

비정상적인 연속된 결제 내역

**한명의 사람이 말도 안되는 짧은 시간에 한 품목을 여러변 결제한 것이다. 그리고 이 결제 관련 내용은 Google Play Console의 주문 관리에서도 확인이 불가능했다. (기록이 남지 않았다.) **

결국 글의 도입부에서 말씀드렸던 것처럼 결제 해킹 문제임을 알게되었고 In-App Purchase Validation에 대해 알아보게 되었습니다.

Subscriptions and In-App Purchases API

Google에서는 **Subscriptions and In-App Purchases API**를 제공하고 있습니다. (전에는 이 API를 “Purchase Status API” 라고 불렀습니다.)

Document를 참고하면 해당 API를 인앱 상품과 구독으로 구성된 앱의 카탈로그를 관리할 수 있으며, 개별 구매에 대한 정보, 구매와 구독의 만료 확인 등, 여러 가지 용도로 사용할 수 있음을 알 수 있습니다.

따라서 실제 결제가 이루어졌고, Google Play Console의 주문 관리에 그 내역이 있는지 확인이 가능한 것입니다. 해당 API는 Google Play Developer API로서 허용되는 사용 할당량이 매일 200,000개의 요청으로 제한됩니다. 이 정도면 충분한 구독, 결제 유효성 검사 요구를 충족시킬 수 있으며, 만약 더 많은 요청이 필요하다면 Google Developer Console에서 요청할 수 있다고 합니다.

API 사용을 위한 서비스 계정 연결

API를 사용하기 위해서는 Google Developer Console에서 서비스 계정을 생성한 후 API 엑세스 권한을 부여해주어야 합니다. 몇가지 단계를 거쳐야 하는데 같이 해보겠습니다.

1.Google Play Console관리자 계정으로 로그인합니다.

2.설정 - API 액세스로 이동합니다. (서비스 약관은 수락합니다.)

3.새 프로젝트 만들기 후 하단의 서비스 계정 만들기를 선택합니다.

4.Google API 콘솔로 이동합니다.

5.서비스 계정 만들기를 선택한 후 다음과 같이 내용을 입력합니다.

6.서비스 계정을 생성하면 자동으로 .json 파일이 다운로드 됩니다. .json 파일에는 API 호출을 위한 인증 정보가 포함되어 있습니다. 관리 및 백업이 필요합니다.

7.Google Play Console로 돌아와, 완료 버튼을 클릭한 후 서비스 계정이 생성되었는지 확인합니다.

8.엑세스 권한 부여를 클릭합니다.

9.역할을 금융으로 선택한 후, 재무 데이터 보기 권한을 설정합니다.
(구매내역 및 영수증 검증을 하기 위해서는 재무 보고서 보기 권한이 필요합니다 . 역할을 금융으로 선택해 주면 해당 권한이 자동으로 선택됩니다. 영수증 검증을 위해서는 금융 역할을 갖는 서비스 계정이 필요합니다.)

API 사용하기 (With node-iap)

복잡하게 토큰을 관리하며 HTTP/REST API를 사용할것 없이 Google은 다양한 언어에 맞게 Client 라이브러리를 제공하고 있습니다. Access Google APIs more easily를 통해서 다양한 언어의 라이브러리를 찾아 사용할 수 있습니다.

google-api-nodejs-client를 사용하면 되지만, **다른 Platform(apple)**에도 대응할 수 있는 node-iap(In-app purchase verification for Apple App Store and Google Play)를 사용하겠습니다.

사용방법은 생각보다 정말 간단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const iap = require('iap');

let platform = 'google';
let payment = {
receipt: 'receipt data', // always required (purchaseToken)
productId: 'abc',
packageName: 'my.app',
keyObject: '', // always required (user auth key)
subscription: false, // optional, if google play subscription
};

iap.verifyPayment(platform, payment, function (error, response) {
/* your code */
});

node-iap는 google과 apple에서 모두 사용가능하기 때문에 platform을 명시해야 합니다. payment에는 확인하고자 하는 인앱결제 내역을 넣습니다.

Android In-App 결제를 하고나면 **purchaseToken**과 **productId**를 알 수 있습니다. payment의 receipt에는 purchaseToken 값을 넣습니다. productId와 packageName을 넣고 **KeyObject**에는 좀전에 사용자 계정을 추가하면서 다운로드 받았던 .json 파일의 값을 넣어주면 됩니다. (require 혹은 import하여 그대로 넣어주면 됩니다.)

Android 단말을 통해 테스트 결제 후 iap를 통해 purchase validation을 하면 다음과 같은 response를 얻을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
{
"receipt": {
"kind": "androidpublisher#productPurchase",
"purchaseTimeMillis": "1410835105408",
"purchaseState": 1,
"consumptionState": 1,
"developerPayload": ""
},
"transactionId": "ghbbkjheodjokkipdmlkjajn.AO-J1OwfrtpJd2fkzzZqv7i107yPmaUD9Vauf9g5evoqbIVzdOGYyJTSEMhSTGFkCOzGtWccxe17dtbS1c16M2OryJZPJ3z-eYhEJYiSLHxEZLnUJ8yfBmI",
"productId": "abc",
"platform": "google"
}

여기서 중요한 부분은 **purchaseState**의 값이 **0**이면 결제가 완료된 상태를 뜻하며 **1**이면 환불이 완료된 상태를 의미합니다.

만약 사용자가 제대로 된 결제를 하지 않는다면 purchaseToken 값은 유효하지 않아 purchase validation 과정에서 err가 발생할 것입니다.

마치며

굉장히 큰 문제라고 생각했지만 생각보다 조치하는 과정에 있어서 큰 어려움은 없었습니다. 아직 제가 더 생각하지 못한 부분이 있을수도 있을거라 생각합니다. 혹시나 더 추가해야 하는 부분이 있다면 알려주세요!

저는 추가적으로 가짜 결제를 시도하는 유저들의 로그를 DB에 남기고 자동으로 block 처리를 해 게임을 못하도록 막았습니다. purchase validation 구현 후 바로 다음날 다시 또 가짜 결제가 이루어졌는데 유효성 검사가 제대로 이루어지는 것을 보고 정말 다행이라 생각했습니다.

제가 지금까지 회사에서 일하며 발생했던 가장 크리티컬했던 부분이라고 생각하는데 혹시나 이 글을 읽으시는 분중 아직 purchase validation을 하지 않고 계시다면 지금이라도 꼭 코드를 추가하셨으면 합니다.

참고

require는 어떻게 동작할까?

Node.js를 사용하며 문득 require에 대해 궁금증이 생겼습니다. 대부분 자주 사용하는 코드를 모듈 형식으로 만들어 **module.exports**를 사용해서 객체 인스턴스를 내보내고 이를 다른 파일에서 **require**를 통해서 사용하게 되는데 대부분 여러 파일에서 모듈을 require해 사용하게 됩니다. 이때 여러파일에서 중복되는 require는 계속해서 새로운 인스턴스를 생성하는지, 그게 아니라면 어떻게 동작되는지 궁금해서 공부하며 찾아본 내용을 포스팅합니다.

Node.js의 모듈 로딩 시스템

Node.js는 간단한 모듈 로딩 시스템을 갖고 있습니다. Node.js에서 파일과 모듈은 일대일로 대응하며 각 파일은 별도의 모듈로 처리됩니다. 그렇기 때문에 여러곳에서 하나의 파일에 작성된 모듈을 필요로 할때 동일한 인스턴스를 사용할 수 있도록 합니다.

즉, 모듈을 require할 때마다 새로운 인스턴스가 생성되는 것이 아니라 캐싱된 객체 인스턴스를 재사용하는 것 입니다.

Node.js 공식 Documentation에서 확인할 수 있듯이 한번 로딩(require)된 모듈은 **require.cache**라는 객체에 캐싱됩니다. key값으로 해당 모듈 파일의 경로를 갖게 되는데 key값이 삭제된다면 다음 require 요청시 다시 재로딩 하게됩니다. 다음 코드를 통해서 require.cache에 캐싱된 모듈을 확인해보겠습니다.

1
2
3
4
5
// foo.js

module.exports = {
foo: "bar"
};
1
2
3
4
5
6
7
8
9
// index.js

var foo = require('./foo');

console.log('---------- require.cache ----------')
console.log(require.cache);

console.log('---------- require.cache keys ----------')
console.log(Object.keys(require.cache));

foo.js 와 index.js 파일을 통해 확인한 결과는 다음과 같습니다.

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
kimjongmin:~/work/require_test $node index.js

---------- require.cache ----------
{ '/Users/kimjongmin/work/require_test/index.js':
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/kimjongmin/work/require_test/index.js',
loaded: false,
children: [ [Object] ],
paths:
[ '/Users/kimjongmin/work/require_test/node_modules',
'/Users/kimjongmin/work/node_modules',
'/Users/kimjongmin/node_modules',
'/Users/node_modules',
'/node_modules' ] },
'/Users/kimjongmin/work/require_test/foo.js':
Module {
id: '/Users/kimjongmin/work/require_test/foo.js',
exports: { foo: 'bar' },
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/kimjongmin/work/require_test/index.js',
loaded: false,
children: [Object],
paths: [Object] },
filename: '/Users/kimjongmin/work/require_test/foo.js',
loaded: true,
children: [],
paths:
[ '/Users/kimjongmin/work/require_test/node_modules',
'/Users/kimjongmin/work/node_modules',
'/Users/kimjongmin/node_modules',
'/Users/node_modules',
'/node_modules' ] } }
---------- require.cache keys ----------
[ '/Users/kimjongmin/work/require_test/index.js',
'/Users/kimjongmin/work/require_test/foo.js' ]

위의 결과에서 확인할 수 있듯이 require.cache 객체는 key값으로 해당 모듈 파일의 경로를 사용해 모듈을 캐싱하고 있습니다.

require가 갖는 문제점

이제 require를 통해 모듈을 로딩할 경우 파일의 경로를 캐시 키로 사용하여 다른 여러 파일에서 동일한 파일을 필요로하는 경우 동일한 캐싱 된 모듈을 사용하는 것을 알게되었습니다.

이로인해 불필요한 메모리 사용을 피할 수 있습니다. 어찌보면 한번 로딩된 후 재사용되기 때문에 싱글 톤과 같이 동작한다고도 생각할 수 있습니다. 그러나 이러한 모듈의 캐싱 방식이 다음과 같이 제대로 동작하지 않는 경우가 있습니다.

  • 파일 이름의 잘못된 대 / 소문자 사용
  • 다른 모듈이 NPM에서 동일한 모듈을 설치할 때

대 / 소문자 구분

Windows 및 macOS는 기본적으로 파일 시스템에서 대 / 소문자를 구분하지 않습니다. 따라서 “foo.js” 라는 파일과 “FOO.js” 라는 파일을 검색 할 경우, 이 두 검색은 실제 파일 이름의 대소 문자와 상관없이 같은 폴더에서 동일한 파일을 찾습니다.

그러나 Node.js에서는 대/ 소문자를 구별하기 때문에 파일 이름을 두 개의 개별 모듈로 취급하므로 “foo.js”와 “FOO.js”가 같은 파일이라는 것을 알지 못합니다.

이 때문에 Windows와 macOS 모두에서 require 호출의 객체 캐시를 쉽게 파기 할 수 있습니다. 다음의 예시 코드에서 쉽게 확인할 수 있습니다.

1
2
3
4
5
// foo.js

module.exports = {
foo: "bar"
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js

var foo = require('./foo');
var FOO = require('./FOO');

console.log('---------- require.cache keys ----------')
console.log(Object.keys(require.cache));

FOO.foo = 'different bar';

console.log('---- foo object ----');
console.log(JSON.stringify(foo, null, 2));

console.log('---- FOO object ----');
console.log(JSON.stringify(FOO, null, 2));

console.log('---- foo object ----');
console.log(JSON.stringify(foo, null, 2));

결과는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kimjongmin:~/work/require_test $node index.js

---------- require.cache keys ----------
[ '/Users/kimjongmin/work/require_test/index.js',
'/Users/kimjongmin/work/require_test/foo.js',
'/Users/kimjongmin/work/require_test/FOO.js' ]
---- foo object ----
{
"foo": "bar"
}
---- FOO object ----
{
"foo": "different bar"
}
---- foo object ----
{
"foo": "bar"
}

결과에서 확인 가능하듯이 require된 모듈은 key값으로 해당 모듈 파일의 경로를 사용해 캐싱되고 있습니다. require시 대 / 소문자를 구분해 key로 사용하기 때문에 2개의 객체가 생성되었으나, 결과적으로는 파일 시스템에 도달하면 같은 파일이 2번 반환된 것입니다. 즉, 같은 파일에 서로 다른 모듈로 2개가 생성되어 있는 것 입니다.

require 문에 파일 이름을 잘못 입력 한 것과 관련된 다른 문제도 있습니다. 대 / 소문자를 구분하는 파일 시스템에 배포하는 경우 실제 파일과 동일하게 처리되지 않은 버전은 파일을 찾지 못합니다.

NPM 모듈 종속성

모듈 캐싱이 제대로 작동하지 않는 상황은 NPM에서 둘 이상의 **모듈 종속성이 같은 모듈을 설치**할 때 입니다. 즉, 프로젝트가 NPM의 “Foo”와 “Bar”에 의존하고 Foo와 Bar가 둘 다 “Baz”에 의존하면 NPM (버전 2 이하)은에 의존하는 각 모듈에 대해 “Baz”의 다른 사본을 설치합니다.

NPM 버전 3 에서는 종속성 목록을 병합하여 문제를 해결하고 있습니다. Foo와 Bar가 둘 다 동일한 Baz의 버전에 의존하면 하나의 사본만 설치합니다.

그러나, Foo와 Bar가 Baz의 서로 다른 (서로 호환되지 않는) 버전을 사용한다면, 여전히 두 버전을 모두 설치하며, 이 경우 모듈 캐시를 공유하지 않습니다.

마치며

반복되는 코드를 모듈화 하거나 각 기능 별로 모듈화 하게되면 결국 다른 파일에서 require를 통해 사용하게 되는데, 이때마다 어떤식으로 동작하게 되는지 궁금했었습니다. 이번 포스팅을 작성하면서 이에 대한 궁금증을 해결할 수 있었고, 결과적으로 한번 로딩된 모듈은 캐싱되어 사용되기 때문에 각기 파일마다 require를 많이 한다고해서 크게 걱정할 필요는 없을 것 같습니다.

또한, 필요에 의해 (필요한 상황이 있을지 모르겠지만…) require.cache에 고의적으로 캐싱된 모듈을 지우고 다시 새로 로딩하여 사용할 수도 있을것 같습니다.

Sequelize 사용하기

ORM 이란?

**관계형 데이터베이스(RDB)**를 사용할때 데이터베이스의 데이터 조작(CRUD)를 위해서는 SQL 문을 작성해야합니다. SQL 문은 비즈니스 로직을 구성하고 있는 코드와 함께 작성하게 되는데 이는 코드의 가독성을 떨어뜨릴뿐만 아니라, 사용하는 관계형 데이터베이스에 따라 조금씩의 차이가 존재하기 때문에 문제가 발생할 수 있습니다. 이를 해결하기 위해 ORM을 사용합니다.

**ORM(Object Relational Mapping)**은 객체(Object)와 관계(Relation)를 맵핑(Mapping)하여 비즈니스 로직에 집중할 수 있도록 데이터 처리 로직을 추상화시킵니다.

객체와 관계를 매핑한다는 것은 데이터베이스에 저장된 레코드를 객체로 바꿔표현한다는 의미하며, 비즈니스 로직에 집중할 수 있도록 데이터 처리 로직을 추상화한다는 것은 쿼리를 사용하지 않고도 데이터베이스를 사용할 수 있음을 뜻합니다.

ORM을 사용할 경우 특정 DBMS에 종속되지 않으며 생산성, 독립성, 가독성(SQL문이 코드에 들어가지 않기때문) 및 유지보수 측면에서의 장점이 있지만 반대로 RAW query에 비해 퍼포먼스가 떨어지고, query가 복잡해 질수록 오히려 생산성이 저하될 수 있다는 단점도 존재합니다.

ORM

Sequelize 설치하기

**Sequelize**는 Node에서 가장 많이 사용되는 ORM 입니다.

RDS로 PostgresSQL, MySQL, MariaDB, SQLite, MSSQL을 지원하고 transaction, read replication등 다양한 기능을 제공하고 있으며. 또한 Promise를 기본으로 동작하기 때문에 비동기 코드를 보기좋게 작성할 수 있습니다.

실습을 위해 express-generator를 통해 Express 프로젝트를 생성후 sequelizemysql module을 설치합니다. (Express 프로젝트를 생성하는 부분은 생략합니다.)

1
npm install --save sequelize mysql

**Sequelize Command Line Interface(CLI)**를 사용하기 위해서 sequelize-cli module을 설치합니다.

1
npm install -g sequelize-cli

Sequelize CLI 사용하기

sequelize cli를 통해서 **migration(마이그레이션), seeder(시더), model(모델)**의 초기 설정을 손쉽게 할 수 있습니다. 이번 포스팅에서는 model에 관해서 알아보겠습니다.

RDB의 테이블을 model로 정의를 하면 해당 model을 통해 데이터 처리가 가능하게 됩니다.
먼저 생성한 express 프로젝트에 sequelize cli 명령어를 통해 sequelize 설정 파일을 생성 후 model을 정의해 보겠습니다.

1
2
sequelize init:config --config config/sequelize.json
sequelize init:models

sequelize cli의 sequelize init:config라는 명령어로 sequelize관련 config 파일을 자동으로 생성할 수 있습니다. 아무런 옵션을 주지 않는다면 config/config.json 파일이 생성됩니다.

sequelize init:models 명령어를 통해서는 models 정의에 관련된 기본 구조를 생성할 수 있습니다.

위 2개의 명령어를 실행하면 다음과 같은 폴더와 파일이 생성됩니다.

1
2
3
4
├── config/
└── sequelize.json
├── models/
└── index.js

Sequelize config 설정하기

sequelize cli를 통해 생성한 config/sequelize.json파일에 데이터베이스에 관련된 설정 값을 입력합니다.

NODE_ENV 에 따라 각기 다른 값을 사용하기 때문에 상황에 맞게 설정할 수 있습니다. NODE_ENV에 대해 잘 알지 못한다면 이곳을 참고하세요.

데이터베이스를 사용한 프로젝트 경험이 있다면 대부분의 config 값은 입력할 수 있습니다. config 값중 dialect에는 사용하는 RDB 이름을 입력해야 합니다. diaect에 사용 가능한 값은 sequelize docs를 참고하세요. 현재 사용 가능한 RDB 로는 ‘mysql’, ‘sqlite’, ‘postgress’, ‘mariadb’가 있습니다.

추가적으로 커넥션 풀과 로깅 기능을 사용한다면 해당 값을 추가합니다. 추가적으로 필요한 옵션은 docs를 참고하세요.

1
2
3
4
5
6
"pool": {
"max": 20,
"min": 0,
"idle": 5000
},
"logging": true

# Model 정의하기

Model을 생성하기 전 sequelize cli를 통해 생성한 models/index.js파일을 살펴보겠습니다. index.js 의 역할은 config/sequelize.json의 설정값을 읽어 sequelize를 생성한 후 models 폴더 아래에 정의한 model 관련 js 파일을 모두 로딩하여 db 객체에 Model을 정의한 후 반환합니다.

sequelize config 관련 파일을 sequelize.json으로 생성하였다면 config 파일을 불러오는 require 부분의 경로를 수정해주어야 합니다.

이제 models 폴더 아래에 간단한 모델을 정의해 보겠습니다. **user.js**를 생성 후 다음의 코드를 입력합니다.

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
module.exports = function (sequelize, DataTypes) {
const user = sequelize.define('User', {
userID: { field: 'user_id', type: DataTypes.STRING(50), unique: true, allowNull: false },
password: { field: 'password', type: DataTypes.STRING(30), allowNull: false },
}, {
// don't use camelcase for automatically added attributes but underscore style
// so updatedAt will be updated_at
underscored: true,

// disable the modification of tablenames; By default, sequelize will automatically
// transform all passed model names (first parameter of define) into plural.
// if you don't want that, set the following
freezeTableName: true,

// define the table's name
tableName: 'user'
});

return user;
};

/*
Sequelize 참고
DataTypes => http://docs.sequelizejs.com/en/v3/api/datatypes/
Associations => http://docs.sequelizejs.com/en/v3/api/associations/
Model Function => http://docs.sequelizejs.com/en/v3/api/model/
*/

Model을 생성하며 사용된 옵션은 주석과 docs를 참고합니다. 이제 모델에 대한 정의가 끝났습니다. 데이터베이스에 user라는 테이블은 User라는 Object로 매핑되었고 user_id, password라는 칼럼은 User Object의 속성으로 매핑되었습니다.

Sequelize Sync 사용하기

Sequeliz에서는 **입력(INSERT), 수정(UPDATE), 조회(SELECT), 삭제(DELETE)**의 **데이터 조작(DML: Data Manipulation Language)**뿐만 아니라 데이터베이스의 스키마 객체를 생성(CREATE), 변경(ALERT), 제거(DROP) 할 수 있는 **데이터 정의(DDL: Data Definition Language)**도 지원합니다.
따라서 이미 만들어진 데이터베이스 테이블에 모델을 매핑할 수 있을 뿐만 아니라, 정의한 모델을 바탕으로 테이블을 생성할 수도 있습니다.(동기화)

해당 기능을 사용하기 위해서는 Sequelize의 sync 메서드를 사용합니다. **app.js**에 다음의 코드를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
// connect To DB
const models = require('./models');
models.sequelize.sync()
.then(() => {
console.log('✓ DB connection success.');
console.log(' Press CTRL-C to stop\n');
})
.catch(err => {
console.error(err);
console.log('✗ DB connection error. Please make sure DB is running.';
process.exit();
});

sync 메서드를 호출하여 실패했을 경우에는 에러 메시지를 출력 후 프로세스를 종료합니다.

sync 메서드는 모델에서 정의한 이름의 테이블이 존재하지 않을 경우에만 동작합니다. 이미 테이블이 존재할 경우에는 models.sequelize.sync({force: true}) 과 같이 force 옵션을 주어 강제적으로 테이블을 제거 후 다시 생성이 가능하지만 매우 위험한 옵션이므로 주의를 기울여 사용해야 합니다.

Sequelize 예제 (SELECT)

이제 Sequelize를 사용하여 SELECT를 사용해보겠습니다. 유저 리스트를 가져오는 query는 다음과 같습니다.

1
2
3
4
5
6
7
models.User.findAll()
.then(results) {
res.json(results);
})
.catch(err => {
console.error(err);
});

User 테이블에 있는 모든 row를 가져오는 query입니다. Sequelize는 결과를 Promise로 리턴하기 때문에 findAll 메서드 역시 Promise를 리턴합니다. 따라서 query의 결과는 then에서 받고, catch문에서 상황에 맞게 error 처리(handling)를 하면됩니다.

findAll의 더 자세한 사용법은 Sequelize-model-findAll 설명을 참고합니다.

Sequelize 예제 (INSERT)

Sequelize를 사용하여 INSERT를 하는 방법은 다음과 같습니다.

1
2
3
4
5
6
7
models.User.create({userID: '유저ID', password: '유저PW'})
.then(result => {
res.json(result);
})
.catch(err => {
console.error(err);
});

create 메서드의 매개변수에 model에서 매핑한 내용을 토대로 데이터를 넣으면 query를 실행 후 insert된 row정보가 반환됩니다.

create의 더 자세한 사용법은 Sequelize-model-create 설명을 참고합니다.

Sequelize 예제 (UPDATE)

User 테이블의 데이터를 수정할때는 다음과 같이 사용합니다.

1
2
3
4
5
6
7
models.User.update({password: '새로운 유저PW'}, {where: {userID: '유저ID'}})
.then(result => {
res.json(result);
})
.catch(err => {
console.error(err);
});

update 메서드의 매개변수에는 update할 데이터를 입력합니다.

update 더 자세한 사용법은 Sequelize-model-update 설명을 참고합니다.

Sequelize 예제 (DELETE)

User 테이블의 데이터를 삭제할때는 다음과 같이 사용합니다.

1
2
3
4
5
6
7
models.User.destroy({where: {userID: '유저ID'}})
.then(result => {
res.json({});
})
.catch(err => {
console.error(err);
});

destroy 메서드의 매개변수에는 where 조건을 입력합니다.(where 조건을 입력하지 않을 경우 테이블의 모든 row가 삭제되기 때문에 주의해야 합니다.)

destroy 더 자세한 사용법은 Sequelize-model-update 설명을 참고합니다.

NVM (Node Version Manager) 사용하기

오늘은 NVM에 대해서 알아보겠습니다. 저는 회사에서 Ionic 을 통해서 하이브리드 앱을 개발하고 있습니다. Ionic의 개발환경은 기본적으로 Node.js를 필요로 합니다. 당시 처음 입사하였을때는 회사에서 사용중인 Node v4.4.0을 설치하여 사용하고 있었습니다. 그러나 후에 여러개의 프로젝트를 하며 ES6 문법을 사용하기 위해 상위 버전의 Node를 사용해야하 하는 일이 생겼습니다. 이런 경우 보통 Node를 제거한 후 상위 버전을 사용하게 된다면 후에 다시 Ionic 프로젝트를 할때 문제가 됩니다. 이럴때 NVM을 사용한다면 여러개의 Node 를 버전별로 쉽게 관리하고 사용할 수 있습니다. 지금부터 NVM을 사용했을때의 이점과 설치 및 사용방법에 대해 알아보겠습니다.

NVM (Node Version Manager)

NVM은 말 그대로 Node의 버전을 관리해주는 매니저입니다. Ruby에는 RVM이 있듯이 Node에서는 NVM을 사용합니다. 한 사용자 계정에 여러개의 Node 버전을 설치하여 선택하여 사용할 수 있습니다.

NVM 사용시 이점

  • 여러 버전의 Node를 쉽게 사용할 수 있습니다. (기존의 버전을 삭제할 필요가 없습니다.)
  • NVM을 사용하지 않고 설치한 Node는 /usr/local/bin/ 경로에 설치되지만 NVM을 사용하여 설치했을 경우에는 /User/kimjongmin/.nvm/versions/node/ 경로에 설치됩니다. NVM을 사용했을 경우 사용자의 종속되어 설치되기 때문에 npm을 통하여 모듈을 설치할 때도 기존과는 달리 -g 옵션을 주지 않아도 설치 가능합니다. (npm또한 Node와 같이 설치되기 때문에 Node 버전마다 다르게 설치됩니다.)
  • Node 버전에 따라 npm도 다르게 설치되기 때문에 모듈의 버전들도 각기 다르게 관리할 수 있습니다. 예를들어, Node v4.4.0 에서는 cordova v5.7.1을 Node v6.6.0 에서는 cordova v6.1.1을 설치하여 사용할 수 있습니다.

NVM 설치

NVM 설치전 기존에 설치되어 있던 Node를 제거하는것을 권장하지만 NVM 설치 후 제거하여도 괜찮습니다.

1
2
curl을 이용하여 설치
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.4/install.sh | bash

설치 후 PATH 정보는 .bashrc에 저장되므로 재로그인 없이 사용하려면 변경된 .bashrc를 다시 적용시켜주어야 합니다.

1
$ source ~/.bashrc

NVM 명령어

1
2
3
// 현재 최신 버전의 Node 설치 (별도의 버전을 지정하지 않고 현재 최신 버전으로 설치합니다.)
$ nvm install node
$ node -v (버전확인)
1
2
3
4
5
6
// 설치된 Node 특정 버전 삭제하기
$ nvm uninstall v4.4.0

```bash
// 설치된 Node는 ~/.nvm/versions/ 경로에서 확인 가능합니다.
$ which node
1
2
// NVM에서 지원하는 Node의 버전을 확인할 수 있습니다.
$ nvm ls-remote
1
2
// 특정 Node 버전 설치는 다음과 같이 가능합니다.
$ nvm install v4.4.0
1
2
// 설치되어 사용가능한 Node 버전 확인
$ nvm ls
1
2
// 특정 Node 버전 사용
$ nvm use v6.6.0
1
2
// 터미널 시작시 노드 기본버전 설정
$ nvm alias default v6.6.0

Tip) 자주쓰는 명령어 alias 등록하기
저는 NVM을 통해 Node v4.4.0과 v6.6.0을 설치하여 사용하고 있습니다. 프로젝트마다 Node 버전이 달라 프로젝트 변경시 Node의 버전도 변경해 주어야 하는 번거로움이 있어 ~.bash_custom에 alias 로 등록하여 사용하고 있습니다. (~.bashrc, ~.bash_profile ) 저는 다음과 같이 사용합니다. (파일에 추가 후 source 명령어로 변경된 파일을 적용하거나 새로운 쉘을 열어야 적용됩니다.)

1
2
3
alias nv='node -v'
alias n4='nvm use v4.4.0'
alias n6='nvm use v6.6.0'

모듈 설치

NVM use 명령어를 통해 원하는 Node 버전을 선택 후 npm install 을 통해 필요한 모듈을 설치할 수 있습니다. 기존 사용법과 동일하나 NVM 은 /User에 설치되기 때문에 더이상 sudo 명령어를 사용하지 않아도 됩니다. 또한 하나의 Node 버전에서 필요한 모듈을 설치 후 새로운 Node 버전을 생성할 때 특정 버전 npm 패키지를 마이그레이션 할 수 있습니다.

1
$ nvm install v6.6.0 --reinstall-packages-from=4.4.0

기존에 설치되어 있던 Node 제거

1
$ sudo rm -rf /usr/local/lib/node /usr/local/lib/node_modules /var/db/receipts/org.nodejs.*

module.exports와 exports 차이 이해하기

모듈이란?

모듈이란 관련된 코드들을 하나의 코드 단위로 캡슐화 하는 것을 말합니다. Node.js 에서 예시를 살펴보겠습니다.
다음과 같은 greeting.js 라는 파일이 있습니다. 이 파일은 두개의 함수를 포함하고 있습니다.

1
2
3
4
5
6
7
8
// greetings.js
sayHelloInEnglish = function() {
return "Hello";
};

sayHelloInSpanish = function() {
return "Hola";
};

모듈 추출하기(exporting)

gretting.js 의 코드가 다른 파일에서 사용될 때 그 효용성이 증가할 것입니다. 이러한 일을 하기 위해서는 다음과 같은 3가지의 단계를 거쳐야 합니다.

1.greeting.js 파일의 코드 첫 부분에 다음과 같은 코드가 존재해야 합니다.

1
2
// greetings.js
var exports = module.exports = {};

2.다른 파일에서 exports 객체를 사용하기를 원한다면 greeting.js 파일에서 다음과 같이 작성해야 합니다.

1
2
3
4
5
6
7
8
9
// greetings.js
// var exports = module.exports = {};

exports.sayHelloInEnglish = function() {
return "HELLO";
};
exports.sayHelloInSpanish = function() {
return "Hola";
};

위의 코드에서 exports 를 module.exports 로 대체할 수 있으며 같은 결과를 얻을 수 있습니다. 이 부분이 잘 이해가 가지 않는다면 exports 와 module.exports 가 같은 객체를 참조한다고 기억하기 바랍니다.

3.module.exports 의 현재 값은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
module.exports = {
sayHelloInEnglish: function() {
return "HELLO";
},

sayHelloInSpanish: function() {
return "Hola";
}
};

모듈 사용하기(importing)

main.js 라는 새로운 파일에서 greeting.js 의 메소드를 사용 할 수 있도록 import 하는 과정은 다음과 같습니다.

1.먼저 require이라는 키워드는 Node.js 에서 module(모듈)을 import(추가) 하기 위해 사용합니다. require는 다음과 같이 정의되어 있습니다.

1
2
3
4
5
6
var require = function(path) {

// ...

return module.exports;
};

2.main.js에서 greetings.js를 require 합니다.

1
2
// main.js
var greetings = require("./greetings.js");

위의 코드는 아래와 동일한 코드 입니다.

1
2
3
4
5
6
7
8
9
10
// main.js
var greetings = {
sayHelloInEnglish: function() {
return "HELLO";
},

sayHelloInSpanish: function() {
return "Hola";
}
};

3.main.js 에서 greeting.js 의 값과 메소드에 접근할 수 있습니다.

1
2
3
4
5
6
7
8
// main.js
var greetings = require("./greetings.js");

// "Hello"
greetings.sayHelloInEnglish();

// "Hola"
greetings.sayHelloInSpanish();

중요 포인트

require 키워드는 object 를 반환합니다. 그리고 module.exports 와 exports 는 call by reference 로 동일한 객체를 바라보고 있고, 리턴되는 값은 항상 module.exports 입니다.

모듈은 기본적으로 객체이고, 이 객체를 module.exports, exports 모두 바라보고 있는데, 최종적으로 return 되는 것은 무조건 module.exports 라는 것입니다.

1
2
3
4
5
6
7
8
9
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res) {
res.render('index', { title: 'Express' });
});

module.exports = router;

위의 소스는 다음과 같이 해석할 수 있습니다.
express.Router() 가 리턴한 “객체”에 일부 프로퍼티를 수정한 뒤, 이 객체 자체를 모듈로 리턴한 것입니다.