코딜리티(Codility) 첫 경험 - 데모 문제 1번

개인적으로 코딩테스트에 대한 공포감(?)이 조금 있다. 사실 코딩테스트에 대해서는 논쟁이 많은 편이다. 개인적으로 코딩테스트는 그냥 최소한의 자격 요건을 보는거지 이 결과로 당락을 결정하는건 아니라고 보는 편이다. 운동하는 사람들끼리 통용되는 말인 노가다에서 쓰는 근육과 헬스에서 쓰는 근육이 다르다이 프로그래밍 세계에서도 그대로 통용된다. 실무에서 쓰는 근육과 코딩테스트에서 쓰는 근육이 다르다 - 즉 코딩테스트 잘본다고 해서 실무를 잘하는게 아니다. 프로그래머라면 물론 코딩도 잘해야 하지만 직급에 맞는 시야와 상호간 커뮤니케이션 능력, 품질에 대한 고려 등등 코딩 외적인 능력이 있어야 훌륭한 개발자가 될 수 있다는게 내 생각이다.

아 근육 얘기 나오니까 하는 얘긴데 코딩테스트 본지 오래된 사람 갑자기 불러다가 코딩테스트 시키면 아무리 천하의 구글러라도 원하는 점수를 못얻을거다. 쓰는 근육이 다르니까.

암튼 말이 길어졌는데 코딩테스트에 대한 공포감을 해결하고자 코딜리티라는걸 이번에 처음 접해보고 한 번 도전해보았다. 코딜리티에는 데모 문제가 있는데 첫 번째 문제인 바이너리 갭(Binary Gap)을 한 번 풀어보았다.

이 문제는 함수를 짜는데 이진수 1과 1 사이에 있는 최대 0의 갯수를 구하는 문제다. 예를 들어 529는 이진수로 1000010001 이며, 함수는 4를 리턴한다. 9는 이진수로 1001이며 함수를 2를 리턴한다. 다만 32는 1000000 이라 갭이 없으므로 0을 리턴하게 된다.

처음에는 뭔가 이진수로 변환한 스트링을 순회하면서 Candidate를 배열에 추가하고 등등등 생각하다가 생각하다가…
갑자기 번뜩이는 split() 함수! 이걸 사용하면 굉장히 쉽게 문제를 해결 -ㅅ-;;;
뭔가 개꼼수 같지만 동작은 잘한다. 테스트케이스 몽땅 통과. 아래 코드는 3분만에 적었다.

알고리즘을 신성시하고 뭔가 대단한걸 기대한 사람들은 날더러 비정통파, 넌 프로그래머도 아냐라고 욕할지도 몰라.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function solution(N) {
// write your code in JavaScript (Node.js 8.9.4)
if (N === undefined || N < 0) {
throw new Error('invalid input');
}

// 10진수를 2진수로
const bin = N.toString(2);

// 1을 delimeter로 쪼갠다.
const sp = bin.split('1');
// 이진수 마지막이 0이면 배열의 마지막을 뺀다.
if (bin.endsWith('0')) {
sp.splice(sp.length - 1, 1);
}

const lens = sp.map((v) => {
return v.length;
});

return Math.max(...lens);
}

물론 이건 1번 문제니까 쉽겠지.

타입스크립트 전면 도입기

회사 개발팀에 사정이 있어서 지난 3월 말부터 개발팀을 이끌게 되었다. 첫 번째 임무는 기존 백엔드의 C#, CQRS, Event-Sourcing, DDD를 걷어내는 일이었다. 좋은 디자인 패턴과 개발방법론과 언어긴 하지만 현 개발팀 아니 한국에서는 좀 쉽지 않은 결정이다. 이유는 많지만 한 마디로 얘기하자면 현 개발팀에 안맞는 옷이었다.

변화가 생긴 개발팀은 Javascript 기반 프론트 1명, 풀스택 2명이었고 C#/Java Entry Level 1명 그리고 나까지 5명이었다. 마음 먹은대로 하자면 백엔드는 Python 혹은 Go로 하고 싶었지만 작은 회사일수록 테크 스택을 통일해야 한다는게 나의 지론이기 때문에 백엔드는 Express를 도입하기로 했다.

Express 도입과 함께 한 가지 도입한 테크 스택은 Typescript다. 현재 웹 프론트엔드인 vue.js와 백오피스 프론트엔드인 react.js는 이미 Typescript였다. 이 참에 Express도 Typescript를 도입하기로 했고 최종적으로 Typescript Rest을 선택했다.

프론트엔드와 백엔드를 모두 Typescript로 통일하고 그 장점을 극대화 하기 위해서 양쪽 사이의 API에서 사용하는 인터페이스나 Enum을 모아놓은 프로젝트를 만들었다. 예를 들어 Item 정보를 만드는 POST API가 있다고 가정하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { POST, Path } from 'typescript-rest';

interface ItemBody {
name: string;
quantity: number;
}

@Path('/item')
export class ItemController {
/**
* 아이템을 만든다.
*/
@Path('')
@POST
public postItem(body: ItemBody){
// .....
}
}

그리고 프론트엔드에서 axios.js로 해당 API를 쓴다면 아래와 같겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ItemBody {
name: string;
quantity: number;
}

//// 생략
class APIs {
public async doSend(item: ItemBody) {
try {
await axios.post(`${BASE_URL}/item`, item);
} catch (e) {
// ....
}
}
}
//// 후략

두 프로젝트에서 공통으로 쓰는 인터페이스인 ItemBody를 별도의 프로젝트로 분리, Private NPM 저장소를 만들고 이걸 import를 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { POST, Path } from 'typescript-rest';
import { ItemBody } from '@mycompany/interface';

/* 이 부분을 없앨 수 있다.
interface ItemBody {
name: string;
quantity: number;
}*/

@Path('/item')
export class ItemController {
/**
* 아이템을 만든다.
*/
@Path('')
@POST
public postItem(body: ItemBody){
// .....
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ItemBody } from '@mycompany/interface';
/* 아래 코드를 없앨 수 있다.
interface ItemBody {
name: string;
quantity: number;
}*/

//// 생략
class APIs {
public async doSend(item: ItemBody) {
try {
await axios.post(`${BASE_URL}/item`, item);
} catch (e) {
// ....
}
}
}
//// 후략

여기서는 프로퍼티가 단순히 2개인 인터페이스만 대상으로 했지만, 프로퍼티가 많아질 수록, 그 인터페이스가 많아질 수록 강력한 효과를 발휘했다. 프론트엔드에서 힘들게 Swagger를 보고 인터페이스를 정의할 필요가 없었고,만들다가 생기는 휴먼 에러도 방지할 수 있었다. 단순히 npm을 업데이트함으로서 API와 싱크를 맞추기도 쉬웠다. 무엇보다 각각의 프로젝트에서 각각 관리하던 인터페이스 코드를 통합관리 할 수 있어서 굉장히 생산성이 높아졌다.

필요성을 느껴서 만들었기 때문에 다른 프로젝트에서는 이렇게 쓰는지는 잘 모르겠지만 프론트엔드와 백엔드 모두 타입스크립트를 쓴다면 한 번 도입해보면 좋겠다.

P.S 두 달 반만에 성공적으로 C#에서 자바스크립트로 이전했다.

현재 개발자 채용 시장에 대한 작은 생각

최근 회사에서 개발자를 뽑고 있다. 2년차 이상 웹 개발자를 뽑기 시작한지 1달이 다 되어가는데 생각보다 이력서가 많이 모이지 않았다. 현재 회사에서는 Node.js, Express, React.js, Mongodb + Mongoose, Typescript, Azure 을 사용하고 있는데 이 개발 스택에 해당하는 개발자가 없는지, 아니면 다른 곳에서 너무 많이 뽑아서 내가 다니는 회사까지 온기가 오기 않는건지 궁금해서 로켓펀치와 윈티드 구인 공고를 훑어봤다.

아래는 나의 주관적인 생각이다. 객관적인 데이터는 없지만 시비 걸지는 말아주시길 : )

백엔드 언어

  1. 백엔드 언어는 Java 35%, Python 20%, PHP 20%, Node.js 10%, C# & Ruby & Go & 기타 15%
  2. 자바가 제일 많다. 토스, 배민, 네이버 등 크고 유명한 곳에서 Spring을 사용하고 있기 때문일거다.
  3. 생각보다 PHP가 많은데 보통은 Laravel 개발자를 뽑는 경우가 많았다. 근데 Laravel이 Full Web Framework라서 편하긴 하지만 이걸로 API 서버를 만들면 성능이 안날텐데… 왜들 쓰는지 모르겠다. Show me the money라면 어느 정도 부하를 처리할 수 있겠지 ㅎㅎ
  4. 파이썬은 Flask는 별로 없고 Django가 많다. 가끔씩 Sanic 쓰는 곳도 있더라. 다만 파이썬은 백엔드 분야 외 데이터 분야 등 다양하게 쓰여서 공고가 많을 지도.
  5. 노드는 생각보다 별로 없다. 좀 의외다. 주로 쓰는 곳은 작은 스타트업.
  6. C#, Ruby, Go, 기타 합해서 15% 정도다. Ruby, C#, Go 언어 순으로 공고가 많았다.
  7. Rust를 메인 백엔드로 쓰는 곳은 아직 본 적이 없다. Go는 간간히 눈에 띄지만 거의 없다.
  8. 한국에서 C#은 쉽지 않겠다.
  9. Ruby는 과거보다 위상이 많이 떨어졌고, 지금도 떨어지는 듯 하다.
  10. 기타로는 Scala가 있다. 데이터 분석 분야와 뱅크샐러드에서 많이 쓰는 듯.

프론트엔드 언어

React 60%, Vue.js 25%, Angular + jQuery 15%

클라우드

AWS 천하통일. 간간히 GCP가 있고 희귀하게 Azure가 있다. 스타트업 중에서는 Azure를 쓰는 곳을 거의 못봤다. 앗! 이것 때문에 지원을 안하는 겐가!

Firebase 한글 문서는 안보는게 좋겠다

지난 2월달에 Google Cloud + Firebase 를 사용해서 펫 프로젝트를 만들고 있을 때였다. 해당 펫 프로젝트는 아래와 같이 테크 스택을 꾸렸다.
API: Google AppEngine - Python
Front: Vue.js
Static Hosting: Firebase Static Web Hosting
DB: Firebase Firestore
이 조합을 선택한 이유는 Firebase Auth를 그대로 JWT로 쓸 수 있어서 또 다른 인증체계가 필요없기 때문이었다. OIDC나 Oauth2를 처리하려면 얼마나 귀찮은가! (그리고 Firebase Static Web Hosting 좋아요. Netlify 도 좋긴한데 파이어베이스 호스팅도 좋습니다.)

이렇게 시간날때마다 API를 만들던 도중 Firestore 쪽에서 문제를 만났다. 유닛 테스트를 하는데 계속 업데이트 API쪽에서 계속 테스트가 실패했다.

코드는 대략 이랬는데,

1
2
3
4
5
inspection_ref = db.collection('inspection').document(commitId)

inspection_ref.update({
'status': 3
}, firestore.CreateIfMissingOption(True))

계속 firestore.CreateIfMissingOption 이 없다고 에러가 발생했다. (참고로 원래 update 동작 방식은 문서가 있으면 덮어쓰는 방식인데 기존 데이터와 병합하고 싶을 때 CreateIfMissingOption을 씁니다). 아 이상하네
https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ko 에 있는 방식대로 그대로 했는데……

구글 놈들이 문서를 잘못 쓸 일은 없고…. Google Cloud API 저장소를 뒤져서 그 이유를 찾아냈다. google-cloud-python 저장소의 4851 PR에서 해당 방식이 merge=boolean 방식으로 변경된 것이다.

즉 아까 예제코드는

1
2
3
inspection_ref.update({
'status': 3
}, merge=True)

식으로 고치니 잘 동작한다. 혹시혹시혹시나 해서 영어판 파이어스토어 문서를 봤더니

여긴 업데이트가 되어있다. 즉, 제대로 업데이트가 안된 한글 문서가 문제였다. 한글 문서는 2018년 5월 29일에 최종 업데이트 되었고, 그 사이 변경된 사항을 업데이트하지 않았다.

곧바로 구글에 이메일을 보냈다. 맨 처음엔 파이어베이스 오른쪽 위에 있는 “의견보내기”를 눌러서 리포트를 했는데 영 답이 없었다. 그래서 아예 파이어베이스 고객지원쪽을 통해서 해당 내옹을 리포트 했다. 물론 담당 엔지니어는 알려줘서 정말 감사하고 빠른 시간내에 리포트해서 고치겠다는 상투적인 답변을 했지만 2019년 2월 19일에 보고했던 문제는 2개월이 다 된 지금도 해결되지 않았다.

앞으로 구글 파이어베이스 문서를 보실 분은 한글문서 보지말고 영어 문서를 보길 바란다. ~~

Vue 패키지 개발하기

작년 말부터 하고 싶었지만 시간이 안나서 못했던 일 - 바로 공식 npm 저장소에 내 라이브러리를 올릴 일이었다. 작년 하반기에는 vue.js + Typescript 로 웹 페이지를 개발하고 있었는데, 그 프로젝트에서 견적서의 금액을 한글로 바꾸는 부분을 꼭 npm에 올리고 싶었다.

그래서 결국 만들어 올렸다. 한 3~4시간 걸린듯하다. 이 패키지가 할 수 있는 일은 숫자를 한글로 변환해준다. 예를 들어 100,1000원은 ‘일백만일천원’ 이런식으로 견적서에서 사용할 수 있는 한글로 변환한다.

이 글은 해당 패키지를 만들고 퍼블리싱까지 한 일을 정리해서 올렸다. 해당 패키지는 vue-cli와 타입스크립트를 써서 만들었다. 워낙 간단한 라이브러리라 타입스크립트는 안써도 된다.

1
2
npm install -g @vue/cli
vue create my-lib

@vue/cli는 create-react-app 과 같이 vue 앱의 스켈레톤을 만들어주는 패키지다. 만들어진 디렉토리로 들어가서 앱을 실행해본다.

1
2
cd my-lib
npm run serve

뷰 기본 앱을 볼 수 있다.

모듈(컴포넌트, 필터) 패키징화

/[project-root]/src 에 filters 디렉토리를 만든다. 그 후 index.ts에 아래의 코드를 작성했다.

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import Vue from 'vue';

/**
* 숫자를 한글로 변환한다.
* @param value Filter 값으로 넘어온 숫자
* @param priceMode true면 변환된 한글에 '일'이 붙는다. 예를 들어 '일십이만삼천원'과 같이 주로 금액을 표시할때 사용한다.
* false라면 '일'이 생략된다. 다만 억과 같이 단독으로 사용했을때 어색한 숫자는 앞에 '일'을 붙인다.
* @returns 한글로 변환된 숫자를 리턴한다. 만약 숫자가 아니거나, 범위를 초과하거나 기타 문제가 발생했을 경우 빈 문자열을 리턴한다.
*/
export function numToKr(value: number, priceMode = true): string {
if (value === 0) {
return '영';
} else if (value === 1) {
return '일';
}

const han1 = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구'];
const han2 = ['', '만', '억', '조', '경', '해'];
const han3 = ['', '십', '백', '천'];

if (!priceMode) {
// 1만 들어올 경우 문제가 생긴다.
if (value !== 1) {
han1[1] = '';
}
}

const numStr = value.toString();
const size = numStr.length;

if (size > 4 * han2.length) {
return '';
}
let kor = '';
let tmp: string[] = [];

for (let i = 1; i < numStr.length + 1; i++) {
const v1 = Number.parseInt(numStr.charAt(i - 1), 10);
if (isNaN(v1)) {
break;
}
if (v1 !== 0) {
tmp.push(han1[v1]);
tmp.push(han3[(size - i) % 4]);
}

if ((size - i) % 4 === 0 && tmp.length !== 0) {
// 일억과 같이 일을 생략하면 어색한 숫자는 '일'을 붙여준다.
kor += tmp.join('');
if (tmp.length === 2 && v1 === 1 && !priceMode) {
kor += '일';
}

tmp = [];
kor += han2[(size - i) / 4];
}
}
return kor;
}

/**
* 숫자로 된 금액을 한글로 변환한다.
* 예) 994950 -> 구십구만사천구백오십원
*/
function install() {
Vue.filter('numberToKor', numToKr);
}

export default install;

해당 모듈은 단순하게 main.ts 혹은 main.js에서 아래와 같이 사용하면 된다.

1
2
3
import NumberToKorFilter from 'vue-number-to-kor';

Vue.use(NumberToKorFilter);

1. 라이브러리 빌드 셋업

먼저 해당 모듈을 타겟으로 잡고 빌드한다. vue-cli를 설치하면 내부적으로 vue-cli-service 를 사용하는데, 이 패키지를 이용하여 해당 모듈을 빌드한다. 기존 package.json은 아래와 같은 형태일 것이다.

1
2
3
4
5
6
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test": "vue-cli-service test:unit"
}

여기에 번들링할 명령어 - build:bundle을 추가한다.

1
2
3
4
5
6
7
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:bundle": "vue-cli-service build --target lib --name vue-number-to-kor ./src/filters/index.ts",
"lint": "vue-cli-service lint",
"test": "vue-cli-service test:unit"
},

2. npm run build:bundle

위 명렁어 실행을 마치면 dist 디렉토리에 umd와 commonjs 용 자바스크립트 라이브러리가 번들링되어 있을 것이다.

3. package.json에 name설정 및 main과 files 추가

이 부분이 중요한데, 반드시 package.json에 main 프로퍼티를 추가해야 한다. umd와 commonjs 중 하나를 선택하면 된다.

1
2
3
{
"main": "./dist/vue-number-to-kor.common.js"
}

그리고 어떤 파일을 패키지에 넣을 것인지 npm에 알려줘야 한다. 이를 package.json에 추가한다.

1
2
3
4
5
6
7
8
9
{
"files": [
"dist/*",
"src/*",
"public/*",
"*.json",
"*.js"
],
}

package.json에 name 속성이 있다. 이게 npm 공식 저장소의 패키지 이름이 되니까 신중하게 결정하도록 한다. 물론 겹치지 않는 걸로 한다.

1
2
3
{
"name": "패키지 이름",
}

4. npm 로그인

npm 공식 저장소에 패키지를 퍼블리싱 하려면 물론 npm에 회원 가입이 되어 있어야 한다. 계정이 없으면 먼저 회원가입을 하자. 그리고 npm login 명령어를 통해서 해당 계정으로 로그인한다. 로그인 상태 확인은 npm whoami를 통해서 할 수 있다.

5. 빌드 및 퍼블리싱

퍼블리싱 전 중간에 코드를 바꾸지 않았으면 다시 빌드 하지 않아도 되지만, 얼마 걸리지 않으니까 그냥 확실하게 빌드 다시 하자.

1
npm run build:bundle

그 다음 npm 공식 저장소에 퍼블리싱을 한다.

1
npm publish --access public

다음 npm공식 저장소에서 해당 패키지가 잘 퍼블리싱 되었는지 확인한다. 이걸로 끝이다.

기타

package.json에는 상당히 많은 정보를 담아야 한다. 기왕 패키지를 배포하는거 다른 개발자가 많이 쓰면 기분이 좋지 않은가.

반드시 기입하기를 권장하는 정보는 version, license, homepage 이며, 넣으면 좋은 정보는 author, keywords, bugs가 있다.

아래는 예시 package.json이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"version": "0.1.1",
"license": "MIT",
"keywords": [
"vue",
"vuejs",
"vue-number-to-kor",
"한글",
"숫자 변환",
"filter"
],
"author": {
"name": "Jongha Kim",
"email": "kim.jongha@gmail.com"
},
"bugs": {
"url": "https://github.com/wisedog/vue-number-to-kor/issues"
},
"homepage": "https://github.com/wisedog/vue-number-to-kor",
}

구글 앱 엔진 파이썬3 변경사항

구글 엡 엔진 파이썬3(Standard Environment)에서는 기존과 많은 점이 변경되었습니다. gVisor 컨테이너 런타임의 영향으로 기존의 Flexible Environment에 가깝게 바뀌었는데요.

최신 런타임

파이썬 2.7 -> 파이썬 3.7

/tmp에 파일 입출력 가능

기존 파이썬2에는 파일 입출력이 불가능해서 /tmp을 쓰는 파이썬 라이브러리가 제대로 작동하지 않는 문제가 있었습니다. 이번 파이썬3 런타임에서는 /tmp에 파일 입출력이 가능합니다.

써드파티 라이브러리 설치 제약 해제

기존 파이썬 2.7 런타임에서는 사용 가능 라이브러리에 제약이 있었습니다. Whitelist 방식으로 사용 가능한 라이브러리 외에는 다 안되는 방식이었는데, 이번에는 어떠한 써드파티 라이브러리의 제약이 없습니다.

쓰레드 사용 가능

쓰레드가 사용가능했습니다. 다만 요청을 완료하기 전까지만 사용할 수 있습니다.

ndb 라이브러리의 퇴장

기존에 datastore를 사용할 때 사용했던 ndb를 사용하지 못하고 google-cloud-datastore 라이브러리를 사용해야 합니다.

로깅 방식의 변경

기존 런타임에서는 logging을 사용하면 바로 스택드라이버에서 사용할 수 있었지만, 새 런타임에서는 명시적으로 스택드라이버를 import 시켜서 로깅해야 합니다. 기존처럼 logging.info, logging.error 찍어도 로그에서 확인할 수 없습니다.

integrated 된 서비스 사용 불가

이 부분이 기존 flexible environment 비슷하게 되었습니다. 파이썬2 Standard environment와 긴밀하게 연결되어 있던 Mail, Search, memcache, Search, Task Queue를 사용할 수 없습니다. 다른 서비스를 사용해야 합니다.

local 개발 환경 변경

기존의 dev_appserver를 사용할 필요가 없습니다. 대신 개발 시 기존 SDK에서 제공하는 기능은 로컬 에뮬레이터를 사용해야 합니다.

URL Fetch out, Requests In

기존에는 앱 엔진 외부 Request는 URL Fetch를 사용해야 했으나 다른 앱처럼 Requests 라이브러리를 사용하세요~

대략적인건 이 정도 인듯 합니다. : )

구글 앱 엔진 2018 하반기 소식 모음

간만에 블로그 글을 씁니다. 2018년 하반기 구글 앱엔진에 굵직한 업데이트가 있었습니다.

Python3 지원

얼마전까지도 파이썬2의 종료일자는 다가오는데 구글측에서는 파이썬3를 지원 예정이라고만 언급해서 애를 태웠습니다. 그런데 2018년 8월 8일, 드디어 파이썬3 런타임을 베타로 지원하기 시작했고 어제인 12월 14일 GA로 올라왔습니다. 지원 런타임은 3.7입니다.

파이썬2에서 파이썬3로 올라오면서 굉장히 많은 변화가 생겼습니다.

기존 파이썬2 앱엔진은 제한된 샌드박스 위에서 구동되는 방식이었습니다. 그래서 사용할 수 있는 라이브러리와 버전도 굉장히 제한적이었고 파일 접근은 물론이요 네트워크까지 제약이 굉장히 많았습니다. 그래서 아무리 장고, 플라스크 앱이라도 구글 앱엔진용으로 수정을 해야 사용할 수 있었습니다.

하지만 파이썬3 앱엔진은 gVisor 콘테이너 런타임 기술을 사용해서 /tmp 를 마음대로 쓸 수 있으며 아무 라이브러리를 설치해서 쓸 수 있습니다.

기본 웹서버를 nginx로 변경

별 영향은 없다네요.

새로운 사용 가용 리전

us-west2(로스 엔젤레스), asia-east2(홍콩) 리전에서 앱 엔진을 사용할 수 있습니다.

참고 : 구글 클라우드

ECMAScript use strict 사용하기

“use strict” 은 자바스크립트 코드를 좀 더 엄격한(strict) 모드에서 실행하도록 인터프리터에게 명령하는 자바스크립트의 지시자(directive)다.

좋은 점 3가지

  1. 코딩 실수 대신 에러를 낸다. 따라서 프로그래머는 자신의 실수를 쉽게 알아차릴 수 있다.
  2. 보통 엄격 모드에서 실행속도가 빠르다라고 알려져 있다. 직접 테스트를 안해봐서 알 수는 없지만, 엄격 모드에서는 인터프리터의 최적화 작업을 어렵게 만드는 문법을 금지시키기 때문에 빠르다.
  3. ECMAScript의 차기 버전들에서 정의 될 문법을 사용하지 못한다.

‘엄격 모드’를 사용하면 생기는 제한

이 ‘엄격 모드’로 자바스크립트 코드를 실행하고자 하면 은은하게 많은 제한이 생긴다. 주석은 크롬 콘솔에서 실행했을 때 발생하는 에러다.

  1. 생성하지 않은 변수를 사용할 수 없다. 자바스크립트에서는 실수로 변수명을 잘못 적었다면 에러를 내지 않고 그냥 새로운 전역 변수가 생성되는데, strict 모드를 사용하면 이를 막을 수 있다.

    1
    2
    "use strict";
    pi = 3.1415; // Uncaught ReferenceError: pi is not defined
  2. 변수나 함수, 삭제할 수 없는 속성을 삭제할 수 없다.

    1
    2
    3
    4
    5
    6
    "use strict";
    var pi = 3.14;
    delete pi;
    function f(p1, p2) {};
    delete f; // Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
    delete Object.prototype; // TypeError: property "prototype" is non-configurable and can't be deleted
  3. 파라미터 이름을 중복으로 쓸 수 없다.

    1
    2
    "use strict";
    function f(p1, p1) {}; // SyntaxError: duplicate formal argument p1
  4. 8진수 리터럴, 8진수 이스케이프 문자를 사용할 수 없다.

    1
    2
    3
    "use strict";
    var oct = 010; // Uncaught SyntaxError: Octal literals are not allowed in strict mode
    var oct1 = "\010"; // Uncaught SyntaxError: Octal escape sequences are not allowed in strict mode.
  5. 읽기 전용 속성이나 GET 전용 속성을 다시 쓸 수 없다.

    1
    2
    3
    4
    5
    6
    "use strict";
    var obj = {get z() {return 0} };
    Object.defineProperty(obj, "x1", {value:123, writable:false});

    obj.x1 = "hello"; // Uncaught TypeError: Cannot assign to read only property 'x1' of object
    obj.z = "world"; // Uncaught TypeError: Cannot set property z of #<Object> which has only a getter
  6. 몇 가지 키워드를 변수명으로 사용할 수 없다.

    1
    2
    3
    "use strict";
    var eval = 'hello'; // SyntaxError: 'eval' can't be defined or assigned to in strict mode code
    var arguments = 'world'; // SyntaxError: 'arguments' can't be defined or assigned to in strict mode code

브라우저 지원

2009년 ECMAScript 5에서 처음 나온 기능으로 IE9과 그 이전 IE에서는 strict 모드를 지원하지 않는다. 다만 이전 브라우져에서 큰 문제는 되지 않는데, 이전 브라우져에서는 문법적(BNF)으로 보면 “use strict”를 그저 리터럴 표현으로 인식하기 때문에 side effect 를 발생시키지 않고 따라서 그냥 무시될 뿐이다.

strict 모드를 지원하는 브라우져는 아래와 같다.

  • 크롬 버전 10 이상
  • 파이어폭스 버전 4 이상
  • 사파리 버전 5.1 이상
  • IE 버전 10 이상

ECMAScript(Javascript) 호이스팅

호이스팅(Hosting)은 자바스크립트 인터프리터가 변수 및 함수의 선언을 항상 코드 상단으로 올리는 행위를 말한다. 엄밀하게 얘기하자면 컴파일 단계에서 변수 및 함수의 선언을 먼저 읽어서 메모리 상에 저장해두는 행위에 가깝다. 예제 코드를 한 번 보자.

1
2
3
a = 1;
console.log('Result:', a);
var a;

이 코드의 실행 결과는 아래와 같다.

1
Result: 1

알다시피 자바스크립트는 변수를 선언하기 전에 사용할 수 있다. 코드 1행에서 선언하지도 않았던 a에 1을 할당했고 2행에서 콘솔에 출력한 결과가 제대로 출력되었다. 정작 변수 a는 3행에서야 나타난다.

이를 호이스팅(hoisting)이라 한다. 호이스팅의 원형 hoist는 사전적 의미로 밧줄이나 장비를 이용하여 끌어올리다는 뜻이 있다. 이렇게 자바스크립트 인터프리터는 컴파일 단계에서 변수 및 함수의 선언을 항상 컨텍스트의 상단으로 끌려올린다. 인터프리터가 선언 코드를 물리적으로 상단으로 올리는 건 아니다. 다만 변수와 함수 선언을 컴파일 단계에서 메모리에 올려두고 undefined 값을 할당한다. 그 후에 코드를 실행하기 때문에 선언이 뒤에 있더라도 선언 앞에서 해당 변수를 사용할 수 있다.

다만 선언에 강조를 한 이유는 초기화 코드는 호이스팅의 대상이 아니기 때문이다.

1
2
3
var a = 5;
console.log('a: %d b: %d', a, b);
var b = 7;

위 코드를 크롬 콘솔에서 실행하면 아래의 결과가 나온다.

1
a: 5 b: NaN

자바스크립트 인터프리터가 위 코드를 호이스팅하면 아래와 같은 형태로 코드를 읽어들인다.

1
2
3
4
5
var a;
var b;
a = 5;
console.log('a: %d b: %d', a, b)
b = 7;

즉 인터프리터는 컴파일 단계에서 변수 a와 b를 메모리 상에 올려두고 undefined 값을 할당해 둔다. 다만 호이스팅은 선언만을 대상으로 하기 때문에 초기화 구문은 해당되지 않는다. 따라서 console로 값을 찍을 무렵에는 a에는 5만 할당되어 있고 b는 undefined 상태가 된다.

함수도 호이스팅의 영향을 받는다. 함수 선언식이 호이스팅의 영향을 받는다. 자바스크립트에서 함수를 사용하는 방식을 잠깐 설명하자면 자바스크립트에서 함수를 사용하는 방식은 함수 선언식(Function Declaration)과 함수 표현식(Function Expression)으로 구분할 수 있다.

함수 선언식은 다른 프로그래밍 언어에서 사용하는 함수 선언식과 동일하다.

1
2
3
function functionDeclaration() {
// 함수 내용
}

함수 표현식은 아래와 같다. Go언어같은 최신 언어에서는 자바스크립트와 같은 함수 표현식을 많이 지원하기도 한다.

1
2
3
var functionExpr = function() {
// 함수 내용
}

예를 들어 아래와 같은 코드가 있다면,

1
2
3
4
5
6
7
8
9
10
funcDecl();
funcExpr();

function funcDecl() {
return 'decl!';
}

var funcExpr = function() {
return 'expr!';
}

호이스팅때문에 인터프리터는 아래와 같이 해석한다.

1
2
3
4
5
6
7
8
9
10
function funcDecl() {
return 'decl!';
}
var funcExpr;
funcDecl();
funcExpr();

funcExpr = function() {
return 'expr!';
}

따라서 funcExpr()를 실행할 시점에는 funcExpr()이 정의되지 않았기 때문에 크롬에서 실행하면 아래와 같은 결과가 나온다.

1
Uncaught TypeError: funcExpr is not a function

이 호이스팅 때문에 의도하지 않은 버그를 내기도 한다. 문제는 자바스크립트 인터프리터의 동작을 이해하지 못하는 사람이 코드를 짜면 버그가 잘 날 수 있다는 사실, 그리고 그 버그가 나오면 찾기 어렵다는 문제가 있다. ~C계열의 undefined behavior의 향기가…
따라서 호이스팅을 막으려면

  1. 선언을 함수나 전역 상단에 적기
  2. let 사용
  3. strict 모드 사용 - use strict 을 사용 시 선언하지 않은 변수를 사용할 수 없다.

구글 앱 엔진 소개

구글 앱 엔진(Google App Engine)은 구글 클라우드 플랫폼에서 구동되는 웹 프레임워크입니다. HTTP/HTTPS 요청을 처리하는 서비스로서 아마존 웹서비스(이하 AWS)의 Beanstalk와 유사합니다. 하지만 이 둘 사이에는 결정적인 차이가 있습니다. 구글 앱 엔진은 완전 관리형(Fully managed) 서비스, AWS는 일부 관리형 서비스라는 점이죠.

쓸만한가요?

근무하고 있는 곳에서는 REST API를 구글 앱 엔진에 올려서 사용하고 있으며, 하루에 수 백 ~ 수 천만건의 요청을 처리하고 있는 중입니다. 일하는 기간 동안에 구글 앱 엔진에서 문제가 발생한 적은 없었습니다. 장애는 주로 DB쪽이나 다른 모듈에서 문제가 발생해서 생긴 적이 대부분이었습니다.

구글 앱 엔진의 장점

쉽고 간단하다

같은 서비스를 EC2를 사용해 구축한다고 하면 Elastic Load Balancer + Auto Scaling Group + EC2 + Route53 설정 등을 해줘야하고 추가적으로 배포도 신경 써줘야 하며, 앱이 죽었는지 살았는지 Health Check도 필요합니다. 하지만 구글 앱 엔진은 이런게 필요없습니다. Load Balancer와 Scaling 설정은 스스로 혹은 간단한 옵션으로 관리하며 인스턴스가 죽으면 자동으로 해당 인스턴스를 자동으로 재시작합니다.

무중단 서비스는 기본이다

내부적으로 Blue Green Deployment를 구현, 새로운 앱을 배포하면 앱 엔진 내부에서 새로운 앱을 배포 후 라우팅을 새로운 앱으로 돌립니다.

강력한 HTTP URL기반 라우팅

개인적으로 매우 마음에 드는 기능입니다. 태그에 의존하는 AWS과는 달리 구글 클라우드는 프로젝트 이름 단위로 리소스가 확실히 구분됩니다. 구글 클라우드에서 example 이라는 프로젝트를 만들었으면 앱 엔진의 URL주소는 https://example.appspot.com이 됩니다. 배포용은 가만히 냅두고 개발용으로 별도의 인스턴스를 만들어서 사용하고 싶을 경우에는 개발용 버전(예: staging)을 배포하면서 이 인스턴스에 트래픽을 주지말라는 옵션을 줍니다. 그리고 개발용 인스턴스에 요청을 보내고 응답을 받고 싶을때는 그냥 https://staging-dot-example.appspot.com 이라고 호출하면 자동으로 개발용 버전으로 해당 트래픽을 연결합니다.

안쓰면 0원 & 지속적으로 사용하면 최대 30% 할인

별도 옵션을 두지 않을 경우 일정 시간동안 요청이 없으면 Instance 갯수가 0이 됩니다. 즉 과금이 없습니다. 펫 프로젝트 할 때 제격이죠. 또한 지속적으로 인스턴스를 켜두면 알아서 30% 정도 깎아줍니다. AWS처럼 스팟 인스턴스니 뭐니 하면서 요금에 신경을 조금 덜 써도 됩니다.

구글 앱 엔진의 단점

한국 리전이 없다

AWS와는 달리 구글 앱 엔진은 한국 리전이 없습니다. 당분간 제일 가까운 리전은 도쿄 리전인데 보통 30 ~ 50ms 정도 Latency를 보입니다. 보통 AWS 서울 리전은 10 ~ 20ms 의 Latency를 보이는 것에 비해서 약간 느린 편이죠.

언어 및 언어 버전에 제약이 있다

지원하는 언어는 Python, Java, PHP, Go 뿐입니다. 아니 구글 앱 엔진 문서에는 C#, Node.js 도 적혀있는데 왜 이러시냐고 묻으실 분들은 아래 조금만 더 읽어주시면 됩니다 : )

프레임워크와 궁합이 좋지 않다

구글 앱 엔진은 완전 관리형 시스템인 PaaS 서비스다보니까, 제약이 상당히 많습니다. Go 언어의 예를 들죠. 일반적인 Go 어플리케이션의 Main은 아래와 같은 형태를 띕니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(":8080", nil))
}

func handle(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
fmt.Fprint(w, "Hello world!")
}

하지만 구글 앱 엔진의 Main의 형태는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"net/http"

"google.golang.org/appengine"
)

func main() {
http.HandleFunc("/", handle)
appengine.Main()
}

func handle(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}

뭐가 다른지 아시겠나요? 웹 어플리케이션은 라우팅 설정을 한 후 특정 포트를 Listen 상태로 들어가는데 앱 엔진에는 이 부분이 없습니다. 따라서 모든 부분을 관리하는 모노리스 웹 프레임워크 중 일부는 웹 엔진에서 제대로 구동이 안될 수 있습니다. 장고(Django) 도 초반에는 앱 엔진에서 제대로 돌릴 수 없었고 Go 언어의 Revel 역시 앱 엔진에서 돌릴 수 없습니다.

Flexible Environment 의 최소 비용이 비싸다

여기서 앞에서 언급한 단점인 ‘언어에 제약이 있다’에 대해서 설명할 수 있습니다. 구글 앱 엔진에서는 Standard Environment(이하 SE)와 Flexible Environment(이하 FE)가 있습니다. 이 둘의 결정적인 차이는 SE는 샌드박스 환경에서 실행되고 FE는 AWS의 EC2와 같은 가상 컴퓨팅 머신 위에서 실행된다는 차이입니다. 따라서 FE는 웹 프레임워크의 제약이 없고 언어의 제약도 많이 없지만 SE는 샌드박스 상에서 돌아갈 수 있는 런타임 위에서만 구동됩니다. 예를 들어 Python 3.x가 십 수년전부터 존재함에도 불구하고 SE상에서는 Python 2.7만 지원하며, PHP7이 나왔음에도 SE에서는 PHP5.5만 지원하지요.

SE는 인스턴스 시간당 비용으로 과금되지만 FE는 CPU, Memory, 디스크 용량의 사용량으로 과금한다는 점이 다릅니다. 암튼 EC2와 같은 가상 컴퓨팅 환경에서 돌아가는게 FE라고 했는데 가장 싼 CPU 타입으로 해도 월 $40을 피할 수는 없습니다. 이는 EC2의 가장 저렴한 인스턴스 타입인 t2.micro가 $8.5에 비한다면 매우 비쌉니다. 개인적으로 이 부분이 가장 아쉬운 부분입니다.

SE와 FE의 차이에 대한 글 : https://cloud.google.com/appengine/docs/the-appengine-environments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×