Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

기술 뉴스 #72 : 17-02-15

웹개발 관련

  • JavaScript Start-up Performance : 크롬 쪽 관점에서 쓴 글이긴 하지만 JavaScript의 초기 구동에 드는 시간을 분석하는 방법과 V8에서 어떻게 개선하고 있고 현재 개선하려고 가능한 방법을 설명한 글이다. JavaScript 파일의 로딩 시간 뿐 아니라 파싱과 컴파일 시간이 중요해진 점을 강조하고 이 시간을 정확히 측정하는 방법과 그동안 V8에서 이 시간을 줄이기 위해서 도입한 기술 등을 설명해서 Code caching, Script Streaming, 사전 컴파일된 JS 코드 등의 방법을 설명하고 있다.(영어)
  • What’s in an AMP URL? : Google에서 AMP 페이지를 볼 때 Original URL, AMP Cache URL, Google AMP Viewer URL 세 가지가 있는데 각 URL이 어떤 의미가 있고 왜 여러 가지 URL을 만들게 되었는지 기술적인 배경을 설명하고 있다. 구현하거나 사용할 때 꼭 인지하고 있어야 하는 부분은 아니지만, 배경지식으로 알고 있으면 좋을 것 같다.(영어)
  • "구글, 'https' 채택 안한 누리집에 안전하지 않은 곳 '낙인'" 기사에 대한 의견 : 크롬 최신 버전에서 HTTPS를 지원하지 않는다고 "안전하지 않음"이라고 표시되는 게 마치 부당하다는 듯 나온 기사에 강성훈 님이 기자한테 해당 기사에 대한 반박내용을 적은 글이다. 해당 글에 왜 HTTPS로 가는 게 중요한지가 배경부터 해서 아주 잘 나와 있고 나도 여기에 아주 공감하고 있다. 이후 네이버나 다음이 HTTPS로 가지 않은 이유가 충분히 있었지만 크롬 때문에 어쩔 수 없이 갔다는 기사도 보았는데 헛소리라고 밖에 보이지 않는다.(한국어)

그 밖의 프로그래밍 관련

  • 리액티브 프로그래밍 대 리액티브 시스템 : Lightbend(구 TypeSafe)의 CTO인 Jonas Bonér와 Viktor Klang가 작성한 Reactive Programming versus Reactive Systems의 번역 글이다. 길지만 읽고 싶었는데 번역해 주신 분이 있어서 쉽게 읽었다. 요즘 화두인 리액티브에서 리액티브 프로그래밍과 리액티브 시스템이 어떻게 다른지 개념을 설명하고 제대로 된 시스템을 만들기 위해서는 둘 다 필요한데 리엑티브 프로그래밍과 리액티브 시스템이 어떻게 상호 보완적인 역할을 하고 있는지 설명하고 있다. 리액티브 프로그래밍은 수명이 짧은 데이터 흐름 체인을 통해 연산하는 데 중점을 둔다. 이것은 주로 이벤트 기반이다. 반면 리액티브 시스템은 (메시징이라고도 하는) 메시지 기반으로 분산 시스템에서 통신과 조정을 통한 복원성과 탄력성에 중점을 둔다.(한국어)
  • Adventures with NPM or: How I Learned to Stop Shrinkwrapping and Love Yarn : npm에서 하위 의존성을 모두 고정하는 shrinkwrap의 문제점을 설명하고 yarn이 제공하는 yarn.lock 파일이 이 문제를 다 해결하고 속도도 좋다고 설명하고 있다. 아직 yarn으로 갈아타지 않았는데 이 글을 보니까 yarn으로 갈아타고 싶다.(영어)
  • Introducing Cloud Spanner: a global database service for mission-critical applications : 구글 클라우드 플랫폼에서 새로 공개한 새로운 데이터베이스로 ACID 트랜잭션을 지원하면서 NoSQL처럼 고가용성과 확장을 지원한다고 한다. 이렇게 보면 부족한 부분이 없어 보일 정도로 CAP 이론이 깨진 건가 싶지만 CAP 이론이 깨진 것은 아니라고 한다.(영어)
  • EVOLVING DISTRIBUTED TRACING AT UBER ENGINEERING : Uber의 아키텍처가 모노리스에서 마이크로서비스 아키텍처로 넘어가면서 기존에 모노리스용으로 만들었던 Merckx를 더는 사용할 수 없게 되고 RPC를 이용한 TChannel을 개발해서 분산추적을 위한 기반을 만든 후 현재의 Jaeger라는 프로젝트를 완성하기까지의 과정을 설명한 글이다.(영어)
  • 이벤트 소싱(Event Sourcing) 소개 : CQRS와 함께 최근에 많이 듣게 되는 이벤트 소싱을 이해하기 위해 이벤트 소싱이 전통적인 방식과 달리 데이터의 상태를 어떻게 관리하는지를 설명한 글이다. 흥미로우면서도 다 이해되진 않지만 이렇게 보다 보면 언젠가 더 이해가 될 때가 올 거라고 생각한다.(한국어)
  • 2/1/17 GitLab.com Database Incident(한글번역) : GitLab.com에서 데이터베이스 장애가 발생하면서 그 복구 과정을 실시간으로 리포팅을 했다. 심지어 YouTube로 복구하는 과정을 라이브로 스트리밍했다.(이는 실수로 알려졌지만, 그 이후에도 계속 방송을 했다) 꽤 심각한 장애 처리 과정을 실시간으로 공유하는 게 꽤 재미있다. 결국 데이터베이스 복구는 실패한 거로 알고 있다. 지금은 GitLab에서 올린 공식 포스트모템도 올라와 있다.(한국어)
  • 중급 파이썬: 파이썬 팁들 : Intermediate Python라는 책을 전체 번역한 온라인 문서다.(한국어)
  • flake8-import-order-spoqa : 스포카에서 Python의 코드 스타일 표준인 PEP8을 맞추는 flake8을 사용하면서 import의 관례를 맞추려고 import-order를 만들어서 사용하다가 최근에 표준도구인 flake8-import-order에서 스포카용 커스텀 import 규칙을 적용할 수 있는 flake8-import-order를 만든 과정을 설명한 글이다.(한국어)
  • AWS KMS를 이용한 암호화 API 구축하기 : AWS가 제공하는 KMS를 이용해서 데이터 암호화를 구현하는 방법을 설명하는 글이다. 여기서는 Lambda와 API Gateway로 API로 만들어서 암복호화를 사용할 수 있도록 구현했다.(한국어)
  • Open Source Guides : GitHub에서 오픈소스 프로젝트에 기여하는 방법과 오픈소스 프로젝트를 운영하는 방법 등에 대한 가이드를 별도의 웹사이트로 제공하기 시작했다.(영어)

볼만한 링크

IT 업계 뉴스

프로젝트

  • Lottie : Airbnb에서 공개한 오픈소스 프로젝트로 After Effect의 애니메이션을 iOS, Android, React Native에서 사용할 수 있게 해주는 라이브러리. 소개 글은 여기서 볼 수 있다.
  • GVFS : Microsoft에서 270GB에 350만개의 파일이 있는 윈도우의 코드 베이스를 git으로 관리하기 위해 일부 파일만 다운로드 받고 파일을 사용할 때 동적으로 파일을 다운받은 GVFS를 오픈소스로 공개했다. IDE 등에서 다른 변경은 필요 없지만 GVFS를 쓰기 위해서 MS에서 커스터마이징한 git을 써야 하는 것으로 보인다. 자세한 내용은 공지에 나와 있고 윈도우 프로젝트를 clone할 때 12시간 이상 걸리고 checkout에만 3시간 걸렸지만 GVFS로 수분/수십 분 정도 내에 할 수 있게 되었다고 한다.
  • Captain : 메뉴바에서 Docker 컨테이너를 관리할 수 있는 macOS 애플리케이션.
  • Traefik : 마이크로서비스 배포를 쉽게 하는 리버스 프락시 서버 및 로드밸런서로 Docker, Swarm, Kubernetes, Marathon, Mesos, Consul 등을 지원한다.
  • Portainer : Docker 호스트와 Swarm 클러스터의 관리 UI 프로젝트.

버전 업데이트

2017/02/15 23:45 2017/02/15 23:45

Vault의 SSH 시크릿 백엔드

최근에 Vault에 관한 글을 계속 올렸는데 PostgreSQL 시크릿 백엔드를 살펴보면 다른 시크릿 백엔드가 어떻게 동작하는지 어느 정도 짐작할 수 있다. 처음 Vault를 살펴보려고 할 때 수동으로 관리하는 비밀 정보를 보관하는 부분이었고 PostgreSQL 시크릿 백엔드 같은 부분은 보고 나서는 괜찮다고 생각하지만 이런 기능을 생각하고 있던 것은 아니다.

개발하면서 비밀정보를 관리하려고 할 때 계정정보나 토큰 외에 관리가 어려운 것 중 하나가 SSH 키이다. 서버에 접속하는 용도로 대부분 SSH 키를 사용하는데 이 파일은 서버에 접속해야 하는 개발자 PC에 저장되어 있어야 하므로 관리가 몹시 어렵다. 큰 회사의 경우에는 서버접속을 위한 게이트웨이 서버를 두고 접속 계정을 따로 관리하기도 하지만 그 정도 규모가 아니면 직접 SSH 키를 관리하게 된다. SSH 키의 사용은 어쩔 수 없으므로 AWS 같은 곳에서 만든 비밀키를 개발자들이 나누어 가지거나 비밀키는 각 개발자의 비밀키를 사용하고 공개키를 서버의 authorized_keys에 등록해서 접속할 수 있게 해야 한다.

두 방법 중 어느 쪽을 사용하더라도 관리가 잘 안 되는 건 매한가지이다. 비밀키를 공유한 경우에는 퇴사하거나 하는 경우 비밀키를 뺏을 수도 없는 노릇이고 authorized_keys 키는 개발자 변경에 따라서 계속 변경하기가 쉽지 않고 금세 방치되고 만다. 그렇다고 개발자가 서버 접속해서 뭔가를 했다는 얘기를 들은 적은 없지만, 보안에 문제가 있는 것은 사실이고 서버 접속용 비밀키가 밖으로 유출되는 건 꽤 민감한 부분이다.

Geofront

현재 일하면서도 SSH 키 관리를 고민하면서 처음에는 spoqa에서 만든 Geofront를 고려했다.1 Geofront에 각 개발자의 공개키를 등록하고 서버 접속을 요청했을 때 임시로 해당 서버 authorized_keys에 공개키를 등록해서 접속할 수 있게 하는 방식이다. 필요할 때 Geofront에서 공개키를 제거하면 되므로 관리도 쉬워진다. 전에 공개할 때 봐서 알고 있는 도구였고 구현한 방식도 사용하기 괜찮아 보였기 때문에 고려대상 1순위였다. 그리고 SSH 키 관리에 대한 이슈는 어느 회사에나 있을 것 같은데 (검색을 잘못했는지 몰라도) 뜻밖에 이런 류의 오픈소스 프로젝트가 많지 않았다.

Vault의 SSH 시크릿 백엔드

비슷한 시기에 Vault도 살펴보고 있었기에 Vault에서 제공하는 SSH 시크릿 백엔드에 자연히 관심이 갔다. Vault를 사용한다는 전제하에 도구를 2개를 사용하는 것보다는 한곳에서 모두 관리하는 게 좋다고 생각했기 때문이다. Vault에도 SSH 시크릿 백엔드를 제공하고 있어서 원격 호스트 접속에 동적으로 SSH 인증을 제공하고 있다.

  1. 일회성 비밀번호(OTP) 방식
  2. 동적 키 방식

Vault는 위 2가지 방식을 제공하는데 문서만 봐서는 어떻게 동작하는지 잘 이해하기가 어려워서 직접 설정을 해보았다.

먼저 Vault 서버가 http://172.10.1.103:8200에 있다고 했을 때 SSH 시크릿 백엔드를 사용하기 위해 먼저 마운트를 한다.

$ vault mount ssh
Successfully mounted 'ssh' at 'ssh'!

$ vault mounts
Path        Type       Default TTL  Max TTL  Description
cubbyhole/  cubbyhole  n/a          n/a      per-token private secret storage
secret/     generic    system       system   generic secret storage
ssh/        ssh        system       system
sys/        system     n/a          n/a      system endpoints used for control, policy and debugging



ssh/에 마운트된 것을 볼 수 있다.

일회성 비밀번호(OTP, One-Time-Password) 방식

일회성 비밀번호를 서버 접속을 요청할 때마다 Vault가 일회성 비밀번호를 알려주고 이 비밀번호를 입력해서 접속하는 방식이다.

vault-ssh-helper 설정

SSH 시크릿 백엔드를 사용하려면 먼저 Vault를 이용해서 접속할 vault-ssh-helper를 설치해야 한다. AWS를 사용한다면 AMI에 미리 설치해서 모든 서버에 자동설치되게 하면 좋을 것이다. vault-ssh-helper도 Go로 작성되었는데 HashiCorp의 릴리스 페이지에서 빌드된 버전을 다운받을 수 있다.(현재 버전은 v0.1.3) 다운받아서 압축을 풀면 vault-ssh-helper 파일이 나오는데 이 파일을 PATH에 추가하고 다음과 같은 설정 파일을 작성한다. 여기서는 config.hcl이라고 하겠다.

vault_addr = "http://13.112.247.219:8200"
ssh_mount_point = "ssh"
tls_skip_verify=true
allowed_cidr_list="0.0.0.0/0"
allowed_roles="otp_key_role"

설정값은 문서를 참고하면 되는데 간단히 설명하면 vault_addr는 사용하는 Vault 서버의 주소이고 ssh_mount_point는 앞에 Vault 서버에서 SSH 시크릿 백엔드를 마운트한 경로이다. 이 두 값을 필수값이다. tls_skip_verify는 데모용이라 TLS을 껐으므로 TLS 확인을 건너뛰도록 한 것이다. 실제 환경이라면 양쪽에서 모두 TLS를 키거나 내부 네트워크를 사용할 때만 이 옵션을 사용해야 한다. allowed_cidr_list는 이 서버에 접속 가능한 CIDR이고 접속할 때 IP가 CIDR 블록에 포함되어 있지 않다면 접속할 수 없다. allowed_roles는 Vault에서 서버에 접속을 허용할 role 이름이고 여럿을 지정하려면 콤마로 구분하면 된다. 이 말은 필요에 따라 서버군별로 role을 나눌 수도 있다는 의미이다.

설정이 잘 되었는지 확인해 보자.

$ vault-ssh-helper -verify-only -config=config.hcl -dev
2017/02/14 16:01:19 ==> WARNING: Dev mode is enabled!
2017/02/14 16:01:19 [INFO] using SSH mount point: ssh

-verify-only는 설정이 유용한지 확인만 할 때 사용하는 옵션이고 위에서 작성한 config.hcl 파일을 설정으로 지정했다. 마지막에 -dev를 사용한 건 TLS를 껐으므로 개발 모드로 실행한 것이다. vault-ssh-helper가 뚫리면 서버가 열리는 것이므로 이 부분이 개발 모드로 강제된 것으로 보이고 TLS를 사용하려면 설정 파일에서 ca_certca_path로 인증서를 지정해주어야 한다.

여기서 끝이면 좋겠지만, SSH 접속을 할 때 vault-ssh-helper를 사용하도록 하는 설정을 추가해주어야 한다.

/etc/pam.d/sshd 파일을 열어서 상단의 내용을 다음과 같이 수정한다.

# Standard Un*x authentication.
#@include common-auth
auth requisite pam_exec.so quiet expose_authtok log=/tmp/vaultssh.log /home/ubuntu/vault-ssh-helper -config=/home/ubuntu/config.hcl -dev
auth optional pam_unix.so not_set_pass use_first_pass nodelay

표준 리눅스 인증 모듈인 @include common-auth부분을 주석 처리하고 그 아래 2줄을 추가했다. 이는 인증을 할 때 vault-ssh-helper를 실행하도록 한 것이다.(/home/ubuntu/vault-ssh-helper -config=/home/ubuntu/config.hcl -dev)

/etc/ssh/sshd_config 파일도 열어서 다음 부분을 찾아서 아래와 같이 수정한다.

ChallengeResponseAuthentication yes
UsePAM yes
PasswordAuthentication no

Ubuntu 16.04에서는 ChallengeResponseAuthenticationUsePAM는 이미 있었고 PasswordAuthentication는 새로 추가했다.

설정이 완료되었으므로 sudo systemctl restart ssh로 SSH를 재시작한다.

Vault의 OTP를 이용한 서버 접속

먼저 Vault에서 OTP로 접속할 role을 생성한다.

$ vault write ssh/roles/otp_key_role \
  key_type=otp \
  default_user=ubuntu \
  cidr_list=0.0.0.0/0
Success! Data written to: ssh/roles/otp_key_role

위에서 otp_key_role로 지정했으므로 ssh/roles/otp_key_role에 새로운 Role을 만드는데 key_typeotp이고 데모에서는 Ubuntu 서버를 사용하므로 default_user로 기본 계정명인 ubuntu를 지정했다. cidr_list은 이 role이 인증을 만들 수 있는 CIDR 블록이다.

이제 접속할 서버에 대한 인증을 만들어야 한다.

$ vault write ssh/creds/otp_key_role ip=13.113.64.123
Key             Value
---             -----
lease_id        ssh/creds/otp_key_role/14fc4dc1-9dae-3079-fedc-670ac19195ae
lease_duration  768h0m0s
lease_renewable false
ip              13.113.64.123
key             e5d60719-b1d4-d6f8-a67f-42da198d950e
key_type        otp
port            22
username        ubuntu

인증 경로는 ssh/creds/otp_key_role가 되고 여기서 ip는 접속할 서버의 IP이다. 이때 이 IP는 앞에서 지정한 otp_key_role의 CIDR 블록에 포함되어야 한다.

접속할 때는 vault를 이용해서 vault ssh -role otp_key_role ubuntu@13.113.64.123와 같이 접속한다. OTP라고 하니까 일회성 비밀번호를 어디서 알려주나 싶지만, 아래와 같이 명령을 실행하면 바로 OTP를 알려준다. 여기서는 ac49f288-ca82-2c60-2567-190bd2a97c0d가 비밀번호이고 복사해서 비밀번호에 붙여주면 서버에 접속할 수 있다.

$ vault ssh -role otp_key_role ubuntu@13.113.64.123
OTP for the session is ac49f288-ca82-2c60-2567-190bd2a97c0d
[Note: Install 'sshpass' to automate typing in OTP]
Password:
Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-59-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  Get cloud support with Ubuntu Advantage Cloud Guest:
    http://www.ubuntu.com/business/services/cloud

0 packages can be updated.
0 updates are security updates.


Last login: Tue Feb 14 15:36:15 2017 from 175.193.64.184
ubuntu@ip-172-31-29-20:~$


동적 키 방식

동적 키 방식은 Geofront의 방식과 비슷하다고 생각하는데 서버관리용 비밀키를 Vault에 등록하고 개발자가 인증을 요청하면 새로운 SSH 키를 생성한 뒤 비밀키는 개발자에게 알려주고 서버의 authorized_keys에 공개키를 등록해주어 접속할 수 있게 하는 방식이다.

서버 설정

각 서버의 설정은 훨씬 간단한 편이다. /etc/sudoers 파일을 열어서(sudo visudo -f /etc/sudoers) 다음 내용을 추가한다.

vaultadmin   ALL=(ALL)NOPASSWD: ALL


Vault에서 SSH 키 발급받기

서버에서 사용하는 마스터 SSH 키를 Vault에 등록한다. 이 키는 서버에 어드민 권한이 있어야 한다.

vault write ssh/keys/dynamic_key key=@test-key.pem
Success! Data written to: ssh/keys/dynamic_key

여기서는 dynamic_key라는 이름으로 등록했고 test-key.pem는 마스터키의 파일명인데 현재 위치에 해당 파일이 존재해야 한다.

$ vault write ssh/roles/dynamic_key_role \
  key_type=dynamic \
  key=dynamic_key \
  admin_user=ubuntu \
  default_user=ubuntu \
  cidr_list=0.0.0.0/0
Success! Data written to: ssh/roles/dynamic_key_role

OTP 때와 같이 role을 만든다. 여기서 admin_user는 Vault가 서버에 접속할 때 사용하는 계정명이고 key는 위에서 설정한 키 이름을 지정하면 된다.

이제 해당 role에서 접속할 서버의 IP로 인증서를 발급받는다.

$ vault write ssh/creds/dynamic_key_role ip=13.112.193.255
Key             Value
---             -----
lease_id        ssh/creds/dynamic_key_role/7182734d-ab58-ba0b-8efe-98d01f71e58f
lease_duration  768h0m0s
lease_renewable true
ip              13.112.193.255
key             -----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCwa1gZT4FincS86tlNSNL2J3VGO6YQMICQOI4DWB5hKtHc7TOZ
odXlb4C61b73KAUN/bHwX1W7aVaQ5MM2Vp6EVkpX9odq/koXj/SkwcdQMTCRv8SY
0ZMhWbv/IY/wQgg8nJI3mvigPI5b7suOEzUPR61UKZHnXjS6kORxmuUT2wIDAQAB
AoGAdh446yFfSI7HVZGMEoGqtaKvk2mGgxpmSamD89tA49/OiTPLs5Y2ZxpjvzQz
WrnRwI9WXtEFzqf1jKeNyEjwch/cOzvggL0ECcp8HeY9QU2tEcLyvriO/9lbeqFG
iNSAmMJijHRgbkqVbhHaJMNT5/xLnn3E6B7ssTeMR8MoetkCQQDT52NvIJ5IURp+
f2CqjcuFzCpJOsEYSq8pEODPLxIStVCalcjNxWMdwAQYPho6IQ4kda5Uwg3NJrGz
yReVRN8dAkEA1SGcfuKXrjbije6LrjCxl+vU3EpMV6MehPcafXmQRj59KdUADoVU
CABtFy1RgsNAR7xxHGg/D079+ESiAIl1VwJALSt7xKp9UwkGzsQ0RObo5WJ5+RYv
JxB0ehqA8WklPxurTOh033geAq91r/089fsp2pfDS4n6CyseYiaRgl4l+QJAfVns
YhBBJ7yuGM4RJx0KhpC0u++S4QRWQdvXn66stTOxh7X395JhLueZQcVsqFzP5KEn
YY7Kb+WEp80t/uTZtwJATa35+aKg7uJggCvYXUSLMM4mEi43Xq47Z7BJEOBTJomo
zBwWP5N+iK0JC/Qkj2WCpf0UgwmntO6JKXn//AlADA==
-----END RSA PRIVATE KEY-----
key_type        dynamic
port            22
username        ubuntu

여기서 key로 나온 부분이 동적으로 발급된 내 비밀키이다. 이 키의 내용을 demo라는 파일에 저장했다면 이 비밀키로 SSH 접속을 하면 서버에 접속할 수 있다.

$ ssh -i demo ubuntu@13.112.193.255
Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-59-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  Get cloud support with Ubuntu Advantage Cloud Guest:
    http://www.ubuntu.com/business/services/cloud

0 packages can be updated.
0 updates are security updates.


Last login: Tue Feb 14 16:39:45 2017 from 175.193.64.184
ubuntu@ip-172-31-16-78:~$

테스트해보니 다른 서버에 키를 요구하면 다른 키가 발급된다. 접속할 때마다 발급받기는 귀찮아 보이고 서버마다 키를 다르게 사용한다는 건 현실적으로 말이 안되서 사실 이 방식은 어떻게 쓰라는지 잘 모르겠다. 그리고 Vault가 서버 접속에는 관여하지 않으므로 Vault의 Audit 로그가 서버 접속까지 감시하지는 못한다.

정리

문서에서는 가능하면 OTP 방식을 권장하고 있다. 동적 키의 경우 테스트해보니 서버마다 키가 새로 발급되므로 실 사용사례가 잘 이해가 되지 않는다. SSH 접속방식은 기존과 같지만 접속할 때마다 발급받아 사용하는 건 귀찮을 테고(만료시간을 짧게 준다면 아마도 이 방식) 서버마다 키를 다르게 쓴다는 건 실무에서 말이 되지 않는다고 생각한다. 그리고 동적 키 방식은 Vault가 인증키 발급에만 관여하고 서버 접속에는 관여하지 않기 때문에 Vault의 Audit 로그가 서버접속을 감시할 수가 없다.


  1. 스포카에서 작성한 https://spoqa.github.io/2014/07/09/geofront.html 참고. 

2017/02/15 02:13 2017/02/15 02:13