반 년 전에 Orvibo S20이라는 스마트 소켓을 구입했다. 스마트 소켓은 스위치를 제어할 수 있도록 Wifi 모듈이 내장되어 있다. 스마트폰 앱을 사용해서 이 소켓의 전원을 올렸다 내렸다 할 수 있는데 집에 있는 거실 스탠드와 안방 스탠드에 연결해서 사용하고 있었다. Orvibo에서는 WiMo라는 앱을 제공하는데 앱이 상당히 부실하고 연결이 오락가락하는 문제점이 있었다. 또한 같은 wifi 네트워크 내에서만 동작하기 때문에 집을 나서면 불을 켜고 나왔는지 확인할 방법이 없었다. 이렇게 생각보다 활용도가 많이 떨어져서 제대로 쓰지 않고 방치하고 있었다.

_DSC0580
_DSC0590

어느 날 거실에서 방을 들어가는데 불을 끄고 스마트폰을 손전등처럼 쓰는 내 모습을 보고는 왜 사놓고 제대로 못쓰고 있나 생각이 확 들었다. 그래서 간단하게라도 만들어보기로 했다.

Orvibo에서는 딱히 개발을 위한 API를 공개하지 않았지만 S20 인터페이스를 리버싱해서 어떤 방식으로 제어할 수 있는지 분석한 사람들이 오픈소스 라이브러리로 내놓기 시작했다. 마침 집에서 방치하고 있던 라즈베리 파이가 있었고 최근 부지런히 사용하고 있는 텔레그램과 연동해서 아주 간단하게 외부에서 제어 가능한 환경을 만들 수 있었다.

사용한 라이브러리

텔레그램 봇을 만들 수 있는 파이썬 패키지는 엄청 많다. python-telegram-bot같은 멋진 라이브러리도 있지만 간단하게 데코레이터 문법으로 작성할 수 있는 pyTelegramBotAPI을 골랐다. 이 패키지는 간단한 기능만 제공하고 있어서 텔레그램의 고급 기능을 사용하려면 다른 패키지를 고르는 것이 낫다.

S20을 제어하는 패키지는 happyleavesaoc/python-orvibo를 사용했다. S20 자체에도 타이머나 추가적인 설정이 가능하지만 이 패키지는 그 기능을 지원하지 않는다. 그래도 자체 기능보다는 프로그램 쪽에서 제어하는게 편해서 제대로 켜고 끌 수만 있기만 하면 되서 이 패키지를 사용하게 되었다.

라우터 설정

라우터에서 설정할 부분은 크게 없었다. 텔레그램 봇은 네트워크 내에서 polling 해오는 방식이라서 별도로 포트를 포워딩해 외부 접근을 허용하거나 할 필요가 전혀 없었다. 대신 편의를 위해서 S20의 IP를 수동으로 할당해 내부에서 고정 IP로 접근할 수 있도록 변경했다.

텔레그램 봇 설정

봇은 예전에 만든 봇을 그대로 사용했다. 만드는 방법은 MS PowerShell에서 텔레그램 메시지 전송하기에서 확인할 수 있다.

코드 작성

멋지진 않지만 그래도 동작하는 코드를 아래처럼 작성했다.

from orvibo.s20 import S20
from telebot import Telebot


bot = TeleBot("<API KEY>")

living_room = ("Living", S20("192.168.3.1"))
bed_room = ("Bed", S20("192.168.3.2"))

def change_switch(s20, mode):
  s20[1].on = mode
  return status_message(s20)

def status_message(s20):
  flag = "on" if s20[1].on else "off"
  return "%s room light is %s" % (s20[0], flag)

@bot.message_handler(commands=["living_room_on"])
def living_room_on(message):
  msg = change_switch(living_room, True)
  bot.send_message(message.chat.id, msg)

@bot.message_handler(commands=["living_room_off"])
def living_room_off(message):
  msg = change_switch(living_room, False)
  bot.send_message(message.chat.id, msg)

@bot.message_handler(commands=["bed_room_on"])
def bed_room_on(message):
  msg = change_switch(bed_room, True)
  bot.send_message(message.chat.id, msg)

@bot.message_handler(commands=["bed_room_off"])
def bed_room_off(message):
  msg = change_switch(bed_room, False)
  bot.send_message(message.chat.id, msg)

@bot.message_handler(commands=["status"])
def all_statuses(message):
  living_msg = status_message(living_room)
  bed_msg = status_message(bed_room)
  bot.send_message(message.chat.id, living_msg)
  bot.send_message(message.chat.id, bed_msg)


if __name__ == '__main__':
  bot.polling()

라즈베리파이 설정

봇에 문제가 생겨도 다시 실행되도록 Supervisor를 사용했다. pip을 사용해서 설치하려 했는데 python3 환경에선 사용하지 못한다고 나왔다. 대신 systemd를 추천 받아서 설치하려는데 라즈비안엔 없어서 고민했는데 apt-get에 Supervisor가 올라와 있어서 쉽게 해결할 수 있었다.

$ sudo apt-get install supervisor

supervisord.conf를 간단하게 작성하고 실행했다. 전체 경로로 지정하고 싶지 않다면 PATH를 추가해도 된다.

[program:bot]
command=/home/pi/bot/.venv/bin/python /home/pi/bot/bot.py
$ supervisord -c /home/pi/bot/supervisord.conf

이제 봇에 말을 걸어보면 동작하는 것을 확인할 수 있다.

telegram s20 bot

문제 해결

S20이 종종 연결이 끊어지는 문제가 있는데 S20으로부터 응답이 없으면 코드에 별도의 에러 핸들링이 없어서 봇이 재시작되었다. 이 경우에는 시간이 지나면 알아서 S20이 다시 붙거나 아니면 S20을 뽑았다 다시 끼워야 한다. 연결에 실패한 것이라도 확인할 수 있도록 코드를 조금 수정했다.

def wrong_message(s20):
  return "%s room light is something wrong" % s20[0]

def change_switch(s20, mode):
  try:
    s20[1].on = mode
  except:
    return wrong_message(s20)
  return status_message(s20)

def status_message(s20):
  try:
    flag = "on" if s20[1].on else "off"
  except:
    return wrong_message(s20)
  return "%s room light is %s" % (s20[0], flag)

실제로 사용해보니 생각보다 조작을 많이 해야해서 타이머를 이용해 일종의 오토메이션 명령어를 추가했다.

import time

@bot.message_handler(commands=["go_to_bed"])
def go_to_bed(message):
  bed_room_on(message)
  bot.send_message(message.chat.id, "Living room light will turn off 15 secs after.")
  time.sleep(15)
  living_room_off(message)


얼마 전 iOS 10 업데이트를 하면서 Home 앱이 기본적으로 들어왔는데 애플의 HomeKit과 연동이 가능하면 사용할 수 있다고 한다. 이 프로토콜을 사용할 수 있도록 구현한 패키지가 nodejs에 존재하는데 HAP-NodeJShomebridge다. 이 도구와 연동하면 Siri를 이용해서 켜고 끌 수 있다는 장점이 있어서 여기서 작성한 코드를 조만간 연동 가능하게 만들어볼 생각이다.

각 장비에서 직접 API를 제공하거나 API를 사용해서 제어할 수 있는 방식에서 마이크로 서비스 아키텍처의 향기가 난다. 대부분의 홈 오토메이션 장비가 게이트웨이를 필요로 한다. 이 글에서는 라즈베리 파이를 썼고 애플의 HomeKit에서는 애플TV가 그런 역할을 한다. 마치 MSA의 API Gateway와 같은 방식으로 말이다. 앞으로 어떤 도구들이 재미있는 API를 들고 나올지 기대된다.

파이썬을 사용하다보면 setup.py와 requirements.txt를 필연적으로 마주하게 된다. 처음 봤을 때는 이 둘의 용도가 비슷하게 느껴져서 마치 둘 중 하나를 골라야 하는지, 어떤 용도로 무엇을 써야 하는지 고민하게 된다. 같은 내용을 이상한모임 슬랙에서 물어봤었는데 Donald Stufft의 글 setup.py vs requirements.txtraccoonyy님이 소개해줬다. 이 두 도구를 사용하는 방식을 명확하게 잘 설명하는 글이라서 허락 받고 번역으로 옮겼다.


setup.py와 requirements.txt의 차이점과 사용 방법

setup.pyrequirements.txt의 역할에 대한 오해가 많다. 대부분의 사람들은 이 두 파일이 중복된 정보를 제공하고 있다고 생각한다. 심지어 이 “중복”을 다루기 위한 도구를 만들기까지 했다.

파이썬 라이브러리

이 글에서 이야기하는 파이썬 라이브러리는 타인이 사용할 수 있도록 개발하고 릴리스하는 코드를 의미한다. 다른 사람들이 만든 수많은 라이브러리는 PyPI에서 찾을 수 있을 것이다. 각각의 라이브러리가 제공될 때 문제 없이 배포될 수 있도록 패키지는 일련의 메타데이터를 포함하게 된다. 이 메타데이터에는 명칭, 버전, 의존성 등을 적게 된다. 라이브러리에 메타데이터를 작성할 수 있도록 다음과 같은 형식을 setup.py 파일에서 사용할 수 있다.

from setuptools import setup

setup(
    name="MyLibrary",
    version="1.0",
    install_requires=[
        "requests",
        "bcrypt",
    ],
    # ...
)

이 방식은 매우 단순해서 필요한 메타 데이터를 정의하기에 부족하지 않다. 하지만 이 명세에서는 이 의존성을 어디에서 가져와 해결해야 하는지에 대해서는 적혀있지 않다. 단순히 “requests”, “bcrypt”라고만 적혀있고 이 의존성이 위치하고 있는 URL도, 파일 경로도 존재하지 않는다. 어디서 의존 라이브러리를 가져와야 하는지 분명하지 않지만 이 특징은 매우 중요한 것이다. 이 특징을 지칭하는 특별한 용어가 있는 것은 아니지만 이 특징을 일종의 “추상 의존성(abstract dependencies)”라고 이야기할 수 있다. 이 의존성에는 의존 라이브러리의 명칭만 사용할 수 있고 선택적으로 버전 지정도 가능하다. 의존성 라이브러리를 사용하는 방식이 덕 타이핑(duck typing)과 같은 접근 방식을 사용한다고 생각해보자. 이 맥락에서 의존성을 바라보면 특정 라이브러리인 “requests”가 필요한 것이 아니라 “requests”처럼 보이는 라이브러리만 있으면 된다는 뜻이다.

파이썬 어플리케이션

여기서 이야기하는 파이썬 어플리케이션은 일반적으로 서버에 배포(deploy)하게 되는 부분을 뜻한다. 이 코드는 PyPI에 존재할 수도 있고 존재하지 않을 수도 있다. 하지만 어플리케이션에서는 재사용을 위해 작성한 부분은 라이브러리에 비해 그리 많지 않을 것이다. PyPI에 존재하는 어플리케이션은 일반적으로 배포를 위한 특정 설정 파일이 필요하다. 여기서는 “배포라는 측면에서의” 파이썬 어플리케이션을 중심으로 두고 살펴보려고 한다.

어플리케이션은 일반적으로 의존성 라이브러리에 종속되어 있으며 대부분은 복잡하게 많은 의존성을 갖고 있는 경우가 많다. 과거에는 이 어플리케이션이 어떤 라이브러리에 의존성이 있는지 확인할 수 없었다. 이렇게 배포(deploy)되는 특정 인스턴스는 라이브러리와 다르게 명칭이 없는 경우도 많고 다른 패키지와의 관계를 정의한 메타데이터도 갖고 있지 않는 경우도 많았다. 이런 상황에서 의존 라이브러리 정보를 저장할 수 있도록 pip의 requirements 파일을 생성하는 기능이 제공되게 되었다. 대부분의 requirements 파일은 다음과 같은 모습을 하고 있다.

# 이 플래그의 주소는 pip의 기본 설정이지만 관계를 명확하게 보여주기 위해 추가함
--index-url https://pypi.python.org/simple/

MyPackage==1.0
requests==1.2.0
bcrypt==1.0.2

이 파일에서는 각 의존성 라이브러리 목록을 정확한 버전 지정과 함께 확인할 수 있다. 라이브러리에서는 넓은 범위의 버전 지정을 사용하는 편이지만 어플리케이션은 아주 특정한 버전의 의존성을 갖는다. requests가 정확하게 어떤 버전이 설치되었는지는 큰 문제가 되지 않지만 이렇게 명확한 버전을 기입하면 로컬에서 테스트하거나 개발하는 환경에서도 프로덕션에 설치한 의존성과 정확히 동일한 버전을 사용할 수 있게 된다.

파일 첫 부분에 있는 --index-url https://pypi.python.org/simple/ 부분을 이미 눈치챘을 것이다. requirements.txt에는 PyPI를 사용하지 않는 경우를 제외하고는 일반적으로 인덱스 주소를 명시하지 않는 편이지만 이 첫 행이 requirements.txt에서 매우 중요하다. 이 내용 한 줄이 추상 의존성이었던 requests==1.2.0을 “구체적인” 의존성인 “https://pypi.python.org/simple/에 있는 requests 1.2.0″으로 처리하게 만든다. 즉, 더이상 덕 타이핑 형태로 의존성을 다루는 것이 아니라 isinstance() 함수로 직접 확인하는 방식과 동일한 패키징 방식이라고 할 수 있다.

추상 의존성 또는 구체적인 의존성에는 어떤 문제가 있을까?

여기까지 읽었다면 이렇게 생각할 수도 있다. setup.py는 재배포를 위한 파일이고 requirements.txt는 배포할 수 없는 것을 위한 파일이라고 했다. 그런데 이미 requirements.txt에 담긴 항목이 install_requires=[...]에 정의된 의존성과 동일할텐데 왜 이런 부분을 신경써야 할까? 이런 의문이 들 수 있을 것이다.

추상 의존과 구체적 의존을 분리하는 것은 중요하다. 의존성을 두 방식으로 분리해서 사용하면 PyPI를 미러링해서 사용하는 것이 가능하게 된다. 또한 같은 이유로 회사 스스로 사설(private) 패키지 색인을 구축해서 사용할 수 있는 것이다. 동일한 라이브러리를 가져와서 버그를 고치거나 새로운 기능을 더한 다음에 그 라이브러리를 의존성으로 사용하는 것도 가능하게 된다. 추상 의존성은 명칭, 버전 지정만 있고 이 의존성을 설치할 때 해당 패키지를 PyPI에서 받을지, Create.io에서 받을지, 아니면 자신의 파일 시스템에서 지정할 수 있기 때문이다. 라이브러리를 포크하고 코드를 변경했다 하더라도 라이브러리에 명칭과 버전 지정을 올바르게 했다면 이 라이브러리를 사용하는데 전혀 문제가 없을 것이다.

구체적인 의존성을 추상 의존성이 필요한 곳에서 사용했을 때는 문제가 발생하게 된다. 그 문제에 대한 극단적인 예시는 Go 언어에서 찾아볼 수 있다. go에서 사용하는 기본 패키지 관리자(go get)는 사용할 패키지를 다음 예제처럼 URL로 지정해서 받아오는 것이 가능하다.

import (
    "github.com/foo/bar"
)

이 코드에서 의존성을 특정 주소로 지정한 것을 볼 수 있다. 이제 이 라이브러리를 사용하다보니 “bar” 라이브러리에 존재하는 버그가 내 작업에 영향을 줘서 “bar” 라이브러리를 교체하려고 한다고 생각해보자. “bar” 라이브러리를 포크해서 문제를 수정했다면 이제 “bar” 라이브러리의 의존성이 명시된 코드를 변경해야 한다. 물론 지금 바로 수정할 수 있는 패키지라면 상관 없겠지만 5단계 깊숙히 존재하는 라이브러리의 의존성이라면 일이 커지게 된다. 단지 조금 다른 “bar”를 쓰기 위한 작업인데 다른 패키지를 최소 5개를 포크하고 내용을 수정해서 라이브러리를 갱신해야 하는 상황이 되고 말았다.

Setuptools의 잘못된 기능

Setuptools는 Go 예제와 비슷한 기능이 존재한다. 의존성 링크(dependency links) 라는 기능이며 다음 코드처럼 작성할 수 있다.

Setup

from setuptools import setup

setup(
    # ...
    dependency_links = [
        "http://packages.example.com/snapshots/",
        "http://example2.com/p/bar-1.0.tar.gz",
    ],
)

이 setuptools의 의존성 링크 “기능”은 의존성 라이브러리의 추상성을 지우고 강제로 기입(hardcode)하는 기능으로 이 의존성 패키지를 정확히 어디에서 찾을 수 있는지 url로 저장하게 된다. 이제 Go에서 살펴본 예제처럼 패키지를 조금 수정한 다음에 패키지를 다른 서버에 두고 그 서버에서 의존성을 가져오는 간단한 작업에도 dependency_links를 변경해야 한다. 사용하는 패키지의 모든 의존성 체인을 찾아다니며 이 주소를 수정해야 하는 상황이 되었다.

다시 사용할 수 있도록 만들기, 같은 일을 반복하지 않는 방법

“라이브러리”와 “어플리케이션”을 구분해서 생각하는 것은 각 코드를 다루는 좋은 방식이라고 할 수 있다. 하지만 라이브러리를 개발하다보면 언제든 _그 코드_가 어플리케이션처럼 될 때가 있다. 이제는 setup.py에 기록한 추상 의존성과 requirements.txt에 저장하게 되는 구체적 의존성으로 분리해서 의존성을 저장하고 관리해야 한다는 사실을 알게 되었다. 코드의 의존성을 정의할 수 있고 이 의존성을 받아오고 싶은 경로를 직접 지정할 수 있기 때문이다. 하지만 의존성 목록을 두 파일로 분리해서 관리하다보면 언젠가는 두 목록이 어긋나는 일이 필연적으로 나타난다. 이런 상황을 해결할 수 있도록 pip의 requirements 파일에서 다음 같은 기능을 제공한다. setup.py와 동일한 디렉토리 내에 아래 내용처럼 requirements 파일을 작성하자.

--index-url https://pypi.python.org/simple/

-e .

이렇게 파일을 작성하더라도 pip install -r requirements.txt 명령을 실행해보면 이전과 다르지 않게 동작하게 된다. 이 명령은 먼저 파일 경로 .에 위치한 라이브러리를 설치한다. 그리고 추상 의존성을 확인할 때 --index-url 설정의 경로를 참조해서 구체적인 의존성으로 전환하고 나머지 의존성을 마저 설치하게 된다.

이 방식을 사용하면 또 다른 강력한 기능을 활용할 수 있다. 만약 단위별로 나눠서 배포하고 있는 라이브러리가 둘 이상 있다고 생각해보자. 또는 공식적으로 릴리스하지 않은 기능을 별도의 부분 라이브러리로 분리해서 개발하고 있다고 생각해보자. 라이브러리가 분할되어 있다고 하더라도 이 라이브러리를 참조할 때는 최상위 라이브러리 명칭을 의존성에 입력하게 된다. 모든 라이브러리 의존성은 그대로 설치하면서 부분적으로는 개발 버전의 라이브러리를 설치하고 싶은 경우에는 다음처럼 requirements.txt에 개발 버전을 먼저 설치해서 개발 버전의 부분 라이브러리를 사용하는 것이 가능하다.

--index-url https://pypi.python.org/simple/

-e https://github.com/foo/bar.git#egg=bar
-e .

이 설정 파일은 먼저 “bar”라는 이름을 사용하고 있는 bar 라이브러리를 https://github.com/foo/bar.git 에서 받아 설치한 다음에 현재 로컬 패키지를 설치하게 된다. 여기서도 의존성을 조합하고 설치하기 위해 --index 옵션을 사용했다. 하지만 여기서는 “bar” 라이브러리 의존성을 github의 주소를 사용해서 먼저 설치했기 때문에 “bar” 의존성은 index로부터 설치하지 않고 github에 있는 개발 버전을 사용하는 것으로 계속 진행할 수 있게 된다.

이 포스트는 Yehuda Katz의 블로그 포스트에서 다룬 Gemfilegemspec에서 영감을 얻어 작성했다.


이 글에서 의존성의 관계를 추상적/구체적인 것으로 구분해서 보는 관점과 그 나눠서 다루는 방식에서 얻을 수 있는 이점이 명확하게 와닿았다.

추상과 대비해서 사용하는 concreate는 “구상”으로 번역하게 되는데 추상에 비해 익숙하지 않아서 구체적으로 번역했다. 만약 구상이 더 편한 용어라면 구체적 의존성을 구상 의존성으로 읽으면 도움이 되겠다.

Python을 실무에서 많이 사용하고 있지 않긴 하지만 사용할 때마다 재미있고 깊게 배우고 싶다는 생각이 늘 드는 언어 중 하나다. 관심을 갖기 시작했을 때부터 PyConAU에 다녀오고 싶었는데 이전엔 브리즈번에서 하고 그 전엔 호바트에서 해서 숙박이나 비행기표나 신경 쓸 일이 많아 고민만 하다 가질 못했었다. 올해는 멜번에서 한다고 하길래 얼리버드 티켓이 열리자마자 구입을 했다. 한동안 python과 가까이 지내지 못해서 과연 내가 이 행사를 참여할 자격이 있는가를 끊임 없이 고민했다. (이미 티켓을 샀으면서도.) 다녀오고 나서는 참 잘 다녀왔구나, 참가 안했으면 얼마나 후회했을까 싶을 정도로 좋은 기억이 남았고 긍정적인 힘을 많이 받고 올 수 있었다.

PyCon AU 2016의 모든 세션은 유튜브에 올라와 있어서 관심이 있다면 확인해보는 것도 좋겠다.

1일차

늘 주말에 일이 있어서 집밖을 나서는 날이 거의 없었는데 오랜만에 아침 일찍 챙겨서 행사장으로 갔고 생각보다 일찍 도착했다. 커피 쿠폰 두 장, 티셔츠를 받았다. 자유로운 분위기에서 대기하다가 들어가서 오프닝 키노트를 봤다.

도착! 이렇게 (공간이) 큰 행사는 처음 ㅜㅜ #pyconau

MicroPython: a journey from Kickstarter to Space

오프닝 키노트는 Python을 작은 임베드 장비에서 구동할 수 있도록 최적화를 먼저 시작하는 방식으로 개발한 구현체인 MicroPython에 대한 이야기였다. 구현을 시작한 과정부터 Kickstarter를 거치며 겪은 경험, BBC와 만든 micro, 그 이후 유럽우주국과 함께 진행한 프로젝트와 앞으로의 로드맵을 이야기했다.

임베딩 장비를 좋아하는데 거기서 고급 언어를 사용할 수 없을까 고민하다가 Python을 공부해야지 하는 마음에 Python을 선택했다는 말도 기억에 남는다. 작게 시작했지만 전환점이 되는 위치가 있었다. 요즘은 이미 거의 완제품이 나온 상태에서 킥스타터에 올리는 분위기지만 MicroPython이 펀딩을 진행했을 당시에는 좀 더 언더(?)같은 느낌이었나보더라. 그렇게 펀딩에 성공해서 MicroPython이 탄생했는데 그게 끝이 아니라 유럽우주국에서 연락이 와 투자를 받았단다. 지금까지 우주에서 사용하는 대부분의 디바이스는 중단 후 컴파일한 소프트웨어를 업데이트하고 다시 구동하는 방식인데 인터프리팅이 가능한데다 고급 언어인 파이썬을 사용할 수 있다는 점에 매력이 컸다고. 유럽우주국과 진행한 프로젝트에서 가능성을 확인했고 추후 장기적인 프로젝트를 계속 진행하게 될거라고 했다.

개발을 시작한 이유도 그렇고 킥스타터나 유럽우주국 이야기, 그렇게 우주로 나가는 디바이스를 만들고 있다는 이야기가 마치 소설처럼 느껴졌다. 작지만 세상을 바꾼다, 느낌으로.

RULES FOR RADICALS: CHANGING THE CULTURE OF PYTHON AT FACEBOOK

페이스북의 이야기였는데 Python2.x에서 Python3.x로 어떻게 옮겼는지에 대한 이야기였다. 2013년부터 이전하자는 이야기를 했는데 2016년에 py3를 기본으로 사용하기까지 과정을 보여줬다.

2013

  • Linter에서 __future__를 자동으로 넣어줘서 사람들이 아무 생각없이 쓰기 시작함.
  • 원래는 gevent와 twist가 py3 지원할 때까지 기다리려 했는데 Thrift에서 py3 지원 시작하면서 이전하기 시작.

2014년

  • 지원하기 시작했는데 아무도 안씀.
  • 구버전 패키징 라이브러리인 fbpackage2를 py3에 맞춰 fbpkg로 작성.
  • Be the change you want to see! 마하트마 간디
  • 기존 라이브러리, 사내 부트스트랩 전부 구버전 기준이라 재작성.
  • 고장난 라이브러리 전부 고침.
  • PyFlakes로 forcing comliance. py3 린트 통과 못하면 커밋 안됨. 자연스레 문법에 친해지기 시작.
  • 문제 생기면 적극적으로 도와줌.

2015년

  • 사내 교육을 py3으로 변경

2016년

  • 기본으로 py3 사용
  • 이제 사람들이 py2를 깨먹기 시작함

자신이 원하는게 있으면 자신이 먼저 변해야 한다는 이야기, 미래지향적으로 교육을 해야한다는 부분, 통계를 잘 수집하면 이런 과정이 더 명료해서 도움이 된다는 점이 기억에 남는다.

Python for Bioinformatics for learning Python

제목만 보고 Bioinformatics에 대한 이야기를 할 줄 알고 들어갔는데 좀 실망했다. Bioinformatics에서 c++를 자주 사용하는데 가르치기 어려워서 파이썬을 사용하기 시작했다고. Bioinformatics에서 분석할 때 사용하기 좋은 함수가 어떤 것인지 설명해주고 코드골프만 실컷 보여줬다.

Linear Genetic Programming in Python Bytecode

제네릭도 아니고 제네틱은 무엇인가 싶어서 들어갔는데 너무 충격적이었다. 이전에 본 적 있던 유튜브 영상으로 시작했다. 이게 바로 제네틱 프로그래밍이라고.

아예 처음 듣는 이야기라서 좀 어렵긴 했는데 간단히 설명하면 진화론 아이디어를 반영한 프로그래밍 방식이었다. 먼저 코드에서 가족군을 만들고 나올 수 있는 경우의 수를 모두 만들어낸다. 그리고 각 코드끼리 비교 평가를 수행하고 적자 선택을 반복적으로 수행해서 목적에 맞는 코드를 만드는 방식이라고. 평가와 선택이 적절하면 세대를 거듭할수록 목적에 맞는 코드가 나온다고 한다. 들으면서 하나도 모르겠는데 이게 엄청 멋진건 알겠더라. 이 프로그래밍으로 이용해서 전자기판 최적화를 하는 대회(Humis contest)가 있다고 한다.

이 제네틱 프로그래밍을 사용할 때는 함축적인 언어를 사용할수록 유용하다고 하며 brainfuck을 예로 들었던 것이 기억난다.

제네틱 프로그래밍은 gp-field-guide.org.uk가 가장 잘 정리되어 있다고 한다.

이 정반합 과정을 Python Bytecode를 사용할 수 있도록 만든 라이브러리 DEAP를 소개했다. 시간이 없어서 Bytecode 예제는 보여주지 못했다.

Security Starts With You: Social Engineering

Social Engineering으로 보안에 취약할 수 있는 정보를 빼내는 방법에 대해 설명했다. 흔히 알고 있는 Phishing 외에도 Tailgating(piggybanking), pretexting, Baiting 등 다양한 방식이 있다고 한다.

기억에 남는 부분은 서비스에서 알림과 같은 이메일을 보낼 때 되도록이면 링크를 넣지 않는 방식으로 하는 것이 보안성이 높다는 이야기였다. 물론 이메일을 받으면 보낸 사람이나 내용을 보면 이미 스팸같은 냄새가 나서 사람들이 안누르긴 하지만 내용이 정말 지능적으로 똑같이 만들어졌다면 평소와 같이 누르게 된다는 것이다. 애초에 메일에 디테일을 담지 않고 자세한 내용은 웹사이트에 로그인하면 확인할 수 있다는 식으로 이메일 자체에 내용을 최소화하는 접근 방식을 사용하면 사람들이 피싱 메일을 받아도 아무 링크나 눌러보지 않도록 하는데 도움을 많이 준다고 한다.

Behind Closed Doors: Managing Passwords in a Dangerous World

password는 신원 확인을 위한 것인데 어떤 방식으로 확인을 하는가, 그 확인을 위한 데이터를 어떻게 보관하는가에 대해서 이야기했다. 보안과 관련한 수많은 라이브러리를 설명해줬다. 점심 먹고 졸릴 시간인데다 라이브러리를 너무 많이 이야기해서 그냥 멍했다. 필요한 권한만 최소로 주라는 말이 기억난다.

CPython internals and the VM

Python을 사용하면서 내부는 어떻게 구현되어 있을까 궁금해서 CPython 코드를 열어봤다는 이야기로 시작해서 코드만 실컷 읽고 끝났다. 오픈소스는 구현이 궁금하면 열어볼 수 있다는 매력이 있다. 어떤 방식으로 돌아가나 그런거 보여줄 줄 알고 갔는데 코드만 읽음.

The dangerous, exquisite art of safely handing user-uploaded files

파일을 업로드 할 수 있는 상황에서 어떻게 올린 파일을 다룰 것인지에 대한 이야기. 파일명이나 파일 확장자는 절대 믿지 말고 고립된 환경에서 파일을 다룰 것에 대해 이야기했는데 PHP를 엄청 깠다. 그다지 새롭지 않았음.

Lightning Talks

5분 짜리 라이트닝 토크를 연속해서 진행했다. pyaudioquitnet을 사용해서 스피커로 데이터를 전송하고 마이크로 데이터를 전송 받는 데모도 재밌었다. 여성 스피커로만 진행하는 KatieConf 이야기도 재밌었다. (패러디지만.) 가장 기억에 남는 토크는 Falsehoods Programmers Have about Identity 였다. 이름, 성(Gender), 생일, 국적에 대해 정보를 받을 때 어떤 방식으로 다양성을 존중해야 하는 것인지에 대한 이야기를 했다. 누군가의 정체성을 부정하는 것은 인간임을 부정하는 것과 마찬가지라는 말이 기억에 남는다.

2일차

Python All the Things

파이썬을 사용해서 앱을 개발할 수 있는 PyBee 소개였다. 언어 구현과 레퍼런스 구현을 분리해서 설명했는데 Django와 같이 재단을 만들어서 운영하는 로드맵을 얘기해줬다. 아직 개발이 한창 진행중이라서 실제로 보여주는 부분은 많이 없었지만 그간 파이썬에서 약하다고 생각했던 부분이라 그런지 사람들 관심이 참 많았다.

Evaluating fire simulators using Docker, Dask and Jupyter notebooks!

호주 기상국(Australia Bureau of Meteorology)에서 발표한 세션인데 주제대로 산불 발생 시뮬레이터를 docker와 dask를 사용해서 구현하고 Jupyter로 결과를 가공하는 과정을 보여줬다. docker 때문에 들어갔는데 생각보다 별 내용 없이 개괄적인 이야기를 많이 했다.

Code the Docs: Interactive Document Environments

문서화에 관한 이야기라고 생각했는데 조금 달랐다. 스피커는 사실 Swift 책을 쓰는 사람이었고 Xcode Playground에서 인터렉티브한 개발 문서를 작성해 프로그래밍을 더 쉽게 이해하고 공부하는데 도움이 된다는 내용이었다. 문서와 코드를 오가면서 실제로 동작하는 코드를 확인하는 과정을 문서 내에서 바로 실행할 수 있는, 동적인 문서 환경을 jupyter와 Playground를 사용해서 보여줬다. Oreilly에서 jupyter를 이 용도로 수정해서 교육 프로그램을 운영하고 있다고 Regex golf를 보여줬는데 영상과 문서, 코드가 잘 동기화되어 있었다. 얘기로는 이런 방식의 미디어가 교육을 위한 책을 대체할 것이라고 했는데 교육을 위한 포맷이 다양해져야 한다는 점은 많이 공감했다.

Deciding between continuity and change in your Open Source community

지금까지 커뮤니티를 운영하고 컨퍼런스를 개최하며 겪은 이야기를 공유했다. 오고 가는 자원봉사자와 어떻게 함께하는지, 커뮤니티에서 어떤 부분을 지향해야 하는지, 커뮤니티에 관한 다양한 주제를 이야기했다. 이상한모임에 참여하면서 생각했던 많은 고민이 우리 만의 문제가 아니라 커뮤니티를 운영하면 누구나 고민하는 부분이구나 생각할 수 있었다. 자전거 그늘처럼 불필요한데 시간과 자원을 많이 소모하는 일을 최대한 피해야 한다는 이야기도 기억에 남는다.

Functional Programming for Pythonistas

루비 개발자인데 파이썬의 함수형 프로그래밍에 대해서 이야기했다. map과 같은 함수랑 lambda 설명하는 수준이었다.

The Evolution of Python Packaging at Facebook

페이스북에서 Python 패키지를 어떤 방식으로 배포하는지에 대한 설명이었다. 좀 더 특별한 방식을 사용할거라 생각했는데 ZIP 압축을 그대로 사용한다고 한다. ZIP을 에디터로 열어보면 PK라는 매직 키워드가 존재하는데 그 앞에 shell script를 넣어도 zip파일엔 영향이 없다고 한다. zip파일에 쉘 스크립트를 포함한 후에 쉘로 그 파일을 실행하면 압축을 해제하기 전에 해야 하는 환경 설정이나 라이브러리 처리를 한 다음 self extract 하는 방식으로 사용한다고 한다. 얼마 전 zip을 대체하는 라이브러리를 페이스북에서 발표했다는 소식이 있었는데 아마 이 환경에서 좀 더 편하게 쓸 수 있도록 개선한게 아닌가 싶다.

그 쉘 스크립트를 포함한 zip을 PAR이라 부르고 페이스북 내에서 사용하는 아카이빙 파일로 표준처럼 사용하고 있다고 한다. 배포해야 하는 서버가 많으니까 별도의 프로그램 없이 최소한의 자원을 써서 설치할 수 있는 환경을 만든 모양이다. 이렇게 만든 파일을 rsync하거나 torrent로 배포한다고. 페이스북이 종종 내려가는 이유가 여기에 있다고 하는데 서버에서 한 번 실행하는 함수라서 작은 환경에서 실행했을 때는 문제가 없다가도 수십 만 대 서버에 배포되면 순식간에 수십 만 번 실행하기 때문이라고 한다.

이 발표에서 페이스북의 문화를 중간 중간 이야기했는데 자신이 하고 싶은 일을 스스로 찾아서 재미있게 한다는 점이 참 인상적이고 부러웠다.

Predicting the TripleJ Hottest 100 With Python

이틀 꼬박 달리니까 좀 지쳐서 쉬는 마음으로 들어간 세션이었다. 모 사이트에서 음악 인기투표를 진행하는데 인기투표 후에 소셜로 공유하는 기능이 있어서 그 내용을 분석해 실제 발표 이전에 해당 발표를 예측하는 서비스를 만든 후기였다. 나는 처음 듣는 사이트였지만 그 순위 가지고 베팅도 하고 그렇다고. raw 데이터를 어떻게 가공하고 분석했는지 설명하는데 그냥 수작업으로 보였다.

Hitting the Wall and How to Get Up Again – Tackling Burnout and Strategies

왜 개발자는 번아웃 되고 힘들게 되는가에 대한 이야기였고 바쁘고 힘든 시기여서 그랬는지 많이 위로가 되었다. 힘든게 당연한 일이고 힘들면 꼭 심리치료사를 찾으라고.

Lightning Talks

역시나 다양하고 재미있는 이야기가 많았다.

  • How well do we (in this room) represent python programmers?: 다양성에 대해 이야기하지만 개발 환경의 다양성에 대한 논의는 적다고. Python을 사용하는 환경이 대부분 리눅스나 맥인 경우가 많은데 실제 학습 환경에서는 윈도를 더 많이 사용하고 있다는 이야기.
  • 5 Languages in 5 minutes: 5가지 언어를 보여줬는데 ARNOLDC, TRUMPSCRIPT, LOLCAT, BRAINF* 4가지와 직접 만든 ANGUISH를 보여줬다. ANGUISH는 공백 언어로 만든 프로그래밍 언어, 매우 안전(?).
  • Open source and social responsibility: 오픈소스에 사용된 코드 라인 수를 보여줬다. Ubuntu, nginx, postgreSQL, python의 코드를 합치면 72,258,475 라인이라고. 이런 코드를 사용하는데 있어서 좀 더 사회적인 책임 의식을 갖고 사용해야 하지 않을까에 대해 이야기했다. 무슨 사회적인 문제를 해결할 수 있을지 짚어 이야기하며 지금 세상을 바꾸라는 이야기로 마무리했다.

개발을 하다보면 눈 앞에 있는 일에 치여서 재미를 느끼지 못할 때가 있다. 세상은 넓고 멋진 사람들을 많은데 내 작업은 작게만 느껴지고 세상을 바꾼다는 포부와는 동떨어진 삶을 사는 기분이 들 때가 있다. 한동안 너무 바빠서 내가 개발을 얼마나 좋아하고 재미있어 하는지, 즐거워하는지 돌아볼 시간조차 없었다는 것을 파이콘에 참석해서야 알게 되었다. 다들 즐겁게, 자신이 하는 일을 이야기하고 공유하는 것을 보고 기분이 많이 좋아졌고 한편으로 위로도 되었다. 부지런히 배워서 컨퍼런스에서 만난 사람들처럼 누군가에게 좋은 영향력을 주는 사람이 되고 싶다.

IMG_1015

IMG_1018

IMG_1019

IMG_1020

IMG_1024

IMG_1022

IMG_1025

IMG_1031

IMG_1032

IMG_1034

IMG_1038

IMG_1057

IMG_1051

IMG_1056

IMG_1060

최근 프로젝트 중 인프라 회사와 함께 협업하는 프로젝트가 있었다. 이전까지 사용하던 방식과는 다르게 상당히 세세한 퍼미션 설정으로 환경을 잡길래 다시 공부하는 겸 정리하게 되었다.

리눅스(또는 Unix-like) 시스템에서는 파일의 접근 권한과 파일의 종류를 16비트에 할당해서 관리하고 있다. 이 퍼미션은 심볼릭 표기법과 8진수 표기법으로 나타낼 수 있다. 흔히 심볼릭 표기법 즉, -rwxr-xr-x와 같은 형태로 접할 수 있다.

파일의 퍼미션은 파일의 상태를 확인하는 명령 stat로 확인할 수 있다.

$ stat hello
  File: 'hello'
  Size: 8760        Blocks: 24         IO Block: 4096   regular file
Device: 2dh/45d Inode: 15170       Links: 1
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2016-08-11 06:56:19.100326163 +0000
Modify: 2016-08-11 06:55:22.698669517 +0000
Change: 2016-08-11 07:23:31.122522096 +0000
 Birth: -

-c, 또는 --format 플래그를 사용해서 원하는 정보만 출력하는 것도 가능하다. 다음처럼 심볼릭 표기법과 8진수 표기법, 소유자, 그룹, 파일명을 확인한다.

$ stat -c "%a %A %U:%G %n" hello
755 -rwxr-xr-x root:root hello

파일 타입

심볼릭 표기법에서 첫 번째 기호는 파일 타입을 의미한다. 이 정보는 16비트 중 4비트에 해당한다.

  • -: 일반 파일
  • d: 디렉토리
  • l: 심볼릭 링크
  • p: Named pipe
  • s: 소켓
  • b, c: 디바이스 파일

그 외에도 Door 등이 존재한다.

접근 권한

접근 권한은 소유자 접근 권한(owner, u), 그룹 접근 권한(group, g), 기타 사용자 접근 권한(others, o)으로 구분된다. 각각의 권한은 읽기(r, 4), 쓰기(w, 2), 실행하기(x, 1)로 구성되어 있다.

각각 3비트로 구성된 3가지 접근 권한으로 총 16비트 중 9비트를 차지한다.

$ chmod 000 hello # 8진수 표기법으로 권한 없음 지정
$ chmod a= hello # 심볼릭 표기법으로 권한 없음 지정
$ chmod 755 hello # 소유자 rwx, 그룹 rx, 기타 rx
$ chmod u=rwx,g=rx,o=rx hello
$ chmod 644 hello # 소유자 rw, 그룹 r, 기타 r
$ chmod u=rw,g=r,o=r hello

특수 권한

특수 권한은 setuid, setgid, 스티키(sticky) 비트가 있다. 16비트 중 앞에서 사용한 파일 타입과 접근 권한을 제외한 나머지, 3비트는 특수 권한을 저장하는 비트로 사용한다.

물론 기본적으로 접근하는 사용자의 권한은 앞에서 설정한 접근 권한을 따르게 된다. 접근 권한에서 접근을 허용하지 않은 상태에서는 아래 특수 권한도 동작하지 않게 된다. 접근 권한과 특수 권한을 조합하면 많은 경우의 수가 나오지만 실행 권한이 없으면 의미가 없는 경우도 있어서 실제로 사용하는 방식은 다소 제한적인 편이다.

특수 권한이 있기 때문에 root 사용자가 아닌 사용자도 ping과 같은 명령을 사용할 수 있게 된다. /tmp와 같은 공유 디렉토리도 특수 권한 중 하나를 사용한다.

Set-user Identification (SUID, setuid, 4)와 Set-group Identification (SGID, setgid, 2)

setuid가 지정되어 있는 파일을 실행하면 해당 파일은 파일 소유자의 권한으로 실행하게 된다. setgid가 지정되어 있는 파일을 실행하면 해당 파일에 지정되어 있는 그룹의 권한으로 실행하게 된다. 물론 애초에 파일에 접근하는 사용자에게 실행하는 권한이 있어야 실행 가능한 것은 마찬가지다. root 권한으로 지정된 프로그램에 이 비트가 적용되어 있다면 실행할 때 root 권한으로 실행 된다는 뜻이다.

이 기능은 보안상 shell script에서는 동작하지 않는다. 실제로 동작을 확인하고 싶다면 작은 C 프로그램을 만들어서 테스트 해볼 수 있다.

#include <stdio.h>

int main () {
  printf("EUID: %d\nUID: %d\nEGID: %d\nGID: %d\n", geteuid(), getuid(), getegid(), getgid());
  return 0;
}

다음은 setuid를 사용한 예다. 같은 그룹 사용자는 해당 파일을 실행할 수 있고 실행할 때 소유자 권한으로 실행하게 된다.

$ chmod 4750 helloworld # 8진수 표기법
$ chmod u=rwxs,g=rx,o= helloworld # 심볼릭 표기법

다음은 setgid를 설정했다. 이 설정은 누구나 파일을 열 수 있으며 helloworld에 지정된 그룹 권한으로 실행한다.

$ chmod 2755 helloworld
$ chmod u=rwx,g=rxs,o=rx helloworld

Sticky 비트 (1)

스티키 비트는 일종의 공유 디렉토리와 같은 권한을 제공한다. 이 비트가 설정되어 있는 상황에서는 접근 권한이 있는 상황에서도 자신의 소유가 아닌 파일을 삭제할 수 없게 된다. 일반적으로 /tmp와 같은 임시 디렉토리에서 사용한다.

다음은 root 계정에서 haruair의 접근 권한이 있는 파일을 생성했지만 스티키 비트가 지정된 폴더 내에 있어서 삭제할 수 없는 예시다.

# mkdir tmp
# chmod 1777 tmp
# chmod a=rwx,=t tmp
# cd tmp ; touch hello
# chmod 777 hello
# sudo -u haruair -i
$ cd tmp ; rm hello
rm: cannot remove 'hello': Operation not permitted

특수 권한 적용하기

chmod를 8진수 표기법과 함께 사용하는 경우에는 주로 3자리로 많이 사용하지만 4자리로 사용하면 첫 번째 숫자가 특수 권한에 해당한다. 즉, chmod 700chmod 0700과 동일하다. 만약 특수 권한이 지정되어 있는 디렉토리에 chmod 700 식으로 지정하면 기존의 특수 권한이 모두 사라지기 때문에 주의해야 한다.

특수 권한으로 인한 보안 문제

특수 권한은 일반적인 파일 접근 방식과 다르기 때문에 사용에 주의해야 한다. 특히 root 권한으로 실행되도록 setuid가 지정된 프로그램에 보안 취약점이 있다면 공격 대상이 될 수 있다.


퍼미션은 가장 기초적인 수준이다. 좀 더 편리하게 사용할 수 있는 ACL도 있고 SELinux를 사용해서 더 보안성 높은 환경을 구축할 수도 있다. 최근에는 서버 가상화로 아키텍처 수준에서의 권한 분리가 이뤄진다. 상황에 맞는 기능을 선택해서 사용하도록 하자.

호주에서도 한국 운전면허증을 NATTI 공증이나 영사 공증 받으면 면허증과 이 공증 서류를 휴대하는 것으로도 운전하는데 아무 지장이 없다. 빅토리아 주에서는 한국 운전면허증를 소지한 경우에 빅토리아 주 운전 면허로 교환해주는 정책이 몇 년 전부터 시작되었는데 다녀오기 귀찮기도 하고 공증 서류만으로도 차량을 렌트해도 전혀 문제가 되지 않아서 바꾸지 않고 있었다. 그런데 영주 비자를 받은 이후 3개월 이내로 바꾸지 않으면 운전 면허를 처음부터 취득해야 한다는 얘기가 있어서 바꾸게 되었다.

VicRoads에 전화(13 11 71)해서 방문 예약을 하면 된다. 한국 운전면허 변경하겠다 하고, 개인 정보 불러주고, 장소 결정하면 예약이 끝난다. 준비할 서류는 다음과 같다.

  • 한국 운전면허증 영사 공증
  • 주소지 증명할 우편물 (유틸리티 빌, 은행 스테이트먼트, TFN 등) – 혹시나 싶어 최대한 챙겨서 갔다.
  • 한국 운전면허증
  • 여권

회사에서 가까운 VicRoads hub@exhibition에 방문했다. 최근에 생긴 오피스라서 상당히 깔끔하고 분위기가 좋았다.

https://www.instagram.com/p/BIsKM1Fj-sD

약속 시간보다 일찍 도착했고 직원 도움을 받아서 신청서를 모두 작성했다. 3년 기한은 77.90불, 10년은 267불이다. 서류 모두 검토하고 사진까지 즉석에서 촬영했다. 운전면허증이 도착하기 전까지는 영수증을 운전면허 대용으로 사용할 수 있다고 한다. 업무일 5일 내로 우편으로 보내준다.

앞으로 얼마나 운전할 일이 있을까 싶기도 하지만 여권 대신 들고 다닐 신분증이니 마음에 평안이 다 생긴다. 😀

신형 맥북 에어가 나오면 구입하려고 그랬는데 생각보다 빨리 나오질 않아서 현행 버전을 구입해야 하는지 고민하고 있었다. 그런데 번역 작업에 MS 워드를 사용하기 시작하면서 윈도 환경이 필요했고 기존 노트북이 워낙에 낮은 사양이라서 작업중에 자주 멎어버렸다. 윈도 환경에서도 이제 Docker도 hyper-v로 사용할 수 있고 조만간 애니버서리 업데이트에서 Bash를 지원할 예정이란 점에서 마음이 많이 갔다.

신형 맥북 에어가 나올지 안나올지도 모르는 상황에 새 맥북에 적용된 키보드라면 별로 구입하고 싶지 않고 16GB 램을 넣어주는 것은 프로 모델에서만 할 것같았다. 이런 추측과 고민 끝에 요즘 MS 스택의 기술에 관심이 많아지고 있는 상황이라 차라리 성능 좋은 윈도 노트북을 구입하기로 마음 먹게 되었다.

윈도 환경의 노트북은 종류가 워낙에 많고 제조사마다 드라이버 최적화 정도도 다르고 서비스 품질도 다른 데다 스펙까지 조금씩 달랐던 탓에 선택의 폭이 워낙 넓었다. 심지어 같은 브랜드인데도 기종별, 세대별로 다 성능이 다른데 깔끔하게 성능을 비교해볼 수 있는 페이지를 제공하는 업체가 정말 적었다. 구입 후보로 골랐던 노트북은 다음과 같다.

  • Dell XPS 13 9350
  • Lenovo X1 Carbon
  • HP Spectre
  • HP Elitebook Folio G1

HP Folio는 정말 얇고 아름답고 구입하고 싶었지만 호주에서 워낙 비싼 데다 높은 사양은 아직 제공하고 있지 않았다. Spectre도 이뻤지만 역시 가격이, X1도 좋았지만 가격이… 이 중에서 i7 6560U, 16GB, SSD 옵션으로 가장 저렴한 모델이 XPS 13이라서 이 기종으로 구입하게 되었다.

애플이 노트북을 소형화 하기 위해 납땜보드를 선보인 이후에 모든 노트북이 램을 납땜하고 있어서 애초에 램을 크게 구입해야 하는 금전적 아쉬움이 있었다. 큰 램을 사용하는 노트북은 SSD도 고용량이고 여러모로 가격이 올라갈 수 밖에 없는 방식으로 가격을 책정해둬서 어쩔 수 없었다. 이 기종이 ubuntu를 기본으로 설치한 developer edition이 있는데 아쉽게도 호주엔 출시하지 않았다. 16GB 모델도 없어서 고민하던 찰라에 델 호주 아울렛 웹사이트에 올라와서 바로 구입했다.

XPS 13 모델은 디스플레이의 베젤이 매우 얇은 인피니티 디스플레이를 채용하고 있어서 다른 13인치 노트북과 비교하면 확연히 크기가 작다. 물론 무게는 좀 나가는 편이고 내가 구입한 QHD+ 터치 디스플레이 모델은 좀 더 무거운 편이라서 크기에 비해 묵직한 느낌이 있다. 이전까지 사용했던 Dell Inspiron 11 3000과 비교했을 때 크기는 거의 비슷하지만 확실히 무거운 편이다. 물론 인스피론은 플라스틱이고 XPS13은 알루미늄인 탓이 크다.

검색해보면 XPS 13의 고질적인 문제가 몇 존재하는데 키보드에 있던 문제는 얼마 전에 키보드를 교체해서 해결했다. 다른 업체는 어떤지 모르겠는데 Dell은 수리 기사분이 출장와서 직접 고쳐주는 On-site 서비스를 제공하고 있어서 편리했다. (iMac 들고 수리하러 지니어스 바까지 들고 갔던 것을 생각하면…) 문제라고 생각했던 부분이 고쳐져서 다행이다.

얼마 사용하지 않았지만 사용하면서 좋았던 점을 정리해보면,

  • QHD 디스플레이가 상당히 만족스러움. 대다수 후기에는 고해상도를 지원하는 프로그램이 적어서 불편하다고 하는데 내가 사용하는 대부분의 프로그램에서는 깔끔하게 나왔음.
  • 크기. 13인치인데 확실히 작다.
  • 내장 스피커. 내장이라는 생각이 안들 정도로 스피커가 괜찮다. 특정 주파수 대역에서 노이즈가 발생하는 문제가 있는데 내 기기에는 문제가 없었다.

단점은,

  • 배터리가 짧음. 디스플레이 탓이라는데 4-5시간 정도 가는 것 같다.
  • 고주파음(high pitch noise). 모델 초기부터 있던 문제라고 하는데 데이터를 읽는 양이 많아지거나 하면 고주파음이 난다. 조용한 공간에서는 소리가 확실히 나서 거슬리는 편이지만 일상적으로는 그냥 무시할만한 수준이다.

맥은 호주오면서 사용하기 시작했고 그 이전까지 윈도 환경을 계속 썼었지만 제대로 윈도 기술 스택을 사용해서 공부해본 적이 없었다. 지금은 관심도 많이 생기고 기술 분야도 다양해져서 윈도 랩탑을 장만한게 괜히 더 기분이 좋아지는 것 같다. 조만간 일들이 좀 정리되고 나면 UWP 등 관심있던 분야를 깊이 있게 공부해보고 싶다.

파워쉘을 가장 처음 접했을 때 확장자에 숫자가 있어서 어떤 의미인지 궁금했는데 오늘 잠시 검색해보고 내용을 정리했다. 먼저 결론을 얘기하면 버전과 상관 없이 .ps1이 파워쉘 스크립트의 확장자다.

파워쉘은 이전까지 Monad Manifesto라는 Windows Command Shell 프로젝트였는데 파워쉘이라는 이름으로 릴리즈 되면서 이전까지 쉘에서 사용하던 .msh 확장자를 .ps1으로 변경했다. .ps는 포스트스크립트(PostScript)의 확장자로 오랜 기간 동안 사용해왔다. 그래서 .ps를 선택할 수 있는 옵션은 아니였던 것으로 보인다. 그리고 버전을 표기하기 위해서 .ps1 이라는 확장자를 선택하게 되었다.

하지만 PowerShell의 2 버전을 내면서 1) 1 버전의 스크립트가 2 버전에서 호환된다, 2) 2 버전이 1 버전을 대체한다는 이유로 .ps1 확장자를 그대로 사용하기로 했다. 대신 각 스크립트에서 요구하는 버전을 선언할 수 있도록 #REQUIRES 문을 제공하게 되었다. 그 이후로는 그냥 .ps1을 아무 고민 없이 사용하게 되었다는 훈훈한 이야기다.

#REQUIRES문을 다음과 같이 추가해서 사용할 버전을 지정할 수 있다.

#REQUIRES -version 5

현재 구동하고 있는 파워쉘보다 높은 버전이라면 다음처럼 오류가 발생한다. (6 버전은 아직 존재하지 않는다. 그냥 큰 숫자를 넣어서 구동한 결과다.)

.\req.ps1 : The script 'req.ps1' cannot be run because it contained a "#requires" statement for Windows PowerShell 6.0. The version of Windows PowerShell that is required by the script 
does not match the currently running version of Windows PowerShell 5.0.10586.122.
At line:1 char:1
+ .\req.ps1
+ ~~~~~~~~~~
    + CategoryInfo          : ResourceUnavailable: (req.ps1:String) [], ScriptRequiresException
    + FullyQualifiedErrorId : ScriptRequiresUnmatchedPSVersion

현재 구동하고 있는 파워쉘의 버전은 $PSVersionTable에서 확인할 수 있다.

PS> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      0      10586  122

Get-Host로 확인할 수 있는 버전은 파워쉘에 접속하는 호스트의 버전을 의미하지 쉘의 버전을 뜻하지 않기 떄문에 주의해야 한다.

만약 상위 호환에 문제가 있는 파워쉘 스크립트를 구동하려면 어떻게 해야 할까? 그런 경우는 PowerShell의 플래그 -Version을 이용해서 구동할 수 있다.

PowerShell -Version 2 .\hello.ps1

실제로 구동 결과를 보면 하위 호환이 지원 되는 가장 마지막 쉘로 구동하는 방식이다. $PSVersionTable을 출력하도록 확인해보면 1, 2 버전은 2 버전으로, 3 버전 이상은 그냥 현재 버전으로 구동하게 된다.

psh 같은 확장자를 선택했으면 좀 멋지지 않았을까 생각이 든다.

더 읽을 거리

얼마 전에 Windows 환경이 필요해 lubuntu 설치해서 사용하던 노트북을 Windows 10으로 전환했다. 이 노트북은 32GB eMMC 내장이라 사실 공간이 엄청 부족한 편이다. Windows 10을 설치하고 나니 5GB만 남아서 Visual Studio는 설치할 엄두조차 내지 못했다. 때마침 Microsoft PowerShell이 정말 좋다는 이야기를 계속 들었던 것이 생각나서 잠깐 살펴보게 되었다. 이런 강력한 쉘이 Windows에 기본 내장인걸 이제야 알았다는 게 분할 정도로 많은 기능이 기본으로 제공되고 있었다. 그래서 몇 가지 간단한 도구를 공부 삼아 만들어봤고 정말 만족스러웠다.

PowerShell Website

Microsoft PowerShell은 Windows XP 이후로 꾸준히 탑재된 명령행 쉘로 .Net Framework으로 개발된 스크립트 언어가 내장되어 있다. 이전에도 JScript나 VBScript가 있었지만 쉘과 연동하기 어려운 문제와 보안 등의 이유로 인해 문제가 계속 제기되었고 그 대안으로 개발된 것이 이 PowerShell이다. .Net Framework과 연동해 다양한 기능을 제공하는 cmdlet과 스크립트, 기본 함수 등은 정말 이 파워쉘 하나만 갖고도 수많은 작업을 쉽게 처리할 수 있을 정도로 탄탄하며 세세하면서도 넓은 영역의 기능을 제공하고 있다. PowerShell Gallery라는 별도의 모듈 관리자도 존재한다. 심지어 dll을 불러서 호출하는 것도 가능하기 때문에 스크립트 언어가 제공하는 기능에 제한받지 않는다는 점이 인상적이다. 너무 자랑만 한 것 같아서 단점은 Windows에서만 제대로 돌아간다는 정도1가 아닐까 싶다. 😛

이 포스트에서는 Microsoft PowerShell을 사용해서 간단한 스크립트를 작성해보고 어떤 방식으로 동작하는지, 얼마나 간편한지 확인(및 영업)을 하려고 한다. 다음은 이 포스트에서 살펴보게 될 내용이다.

  • 텔레그램 봇을 생성하고 토큰 발급하기
  • 텔레그램 봇의 정보를 API로 확인하기 (getMe, getUpdates)
  • 봇 API의 token과 사용자의 chat_id를 설정 파일로 분리하기
  • 웹페이지 호출한 다음 결과물 가공하기
  • 텔레그램 봇 API로 메시지 전송하기
  • 새 메시지만 전송하도록 메시지 비교하고 csv로 저장하기

이 글에서 필요한 준비물은 다음과 같다.

  • PowerShell 구동 가능한 Windows 환경
  • 메신저 서비스인 텔레그램(telegram) 계정

전체 코드는 haruair/ps-telegram-message에서 확인할 수 있다.

텔레그램 봇 생성하기

텔레그램 봇을 생성하려면 @BotFather 계정에 대화를 신청해서 쉽게 생성할 수 있다. telegram.me/BotFather 링크를 누르거나 @BotFather를 직접 검색해서 추가한다. 봇을 생성하면 이 봇을 사용할 때 넣어야 하는 API 토큰을 발급해주는데 모바일에서는 컴퓨터로 복사하기 불편할 수 있다. 텔레그램 웹에서 생성하면 편리하다.

@BotFather에게 /newBot을 입력하면 새로운 봇 이름과 봇 계정명을 순서대로 입력하라고 안내한다. 순서대로 입력하고 나면 API 토큰과 해당 봇 링크를 알려준다.

botfather

이 토큰을 이용해서 API를 사용하는 방법은 Telegram Bots에서 자세히 확인할 수 있다. (참고로, 위 이미지의 API 토큰은 더이상 동작하지 않는다.)

텔레그램 봇 정보 확인하기

앞에서 생성한 토큰을 사용해서 텔레그램 봇에 정상적으로 접근할 수 있는지 확인하려 한다. 파워쉘은 ps1이라는 확장자를 사용한다. status.ps1라는 파일을 다음 내용으로 작성한 후에 저장한다.

$response = Invoke-WebRequest -Uri "https://api.telegram.org/bot<API 토큰>/getMe"
echo $response.RawContent
pause

텔레그램 봇 API의 getMe 메서드 주소로 요청을 보내는 코드를 작성했다.

이 코드에서 Invoke-WebRequest 라는 함수를 확인할 수 있다. 이런 함수는 PowerShell 환경에서만 사용할 수 있도록 구현된 함수로 Cmdlet으로 부른다. (Command-let으로 읽는다.) Invoke-WebRequest cmdlet은 HTTP, HTTPS, FTP 또는 FILE 프로토콜로 웹페이지나 웹서비스에 요청을 보낼 수 있는 기능을 제공한다. 이 함수가 반환한 HtmlWebResponseObject 개체를 $response에 저장했다.

Invoke-WebRequest에서 오류가 발생하면 포스트 마지막에 있는 문제 해결을 참고한다.

2행에서 echo를 사용해서 $responseRawContent 프로퍼티를 출력한다. echoWrite-Output cmdlet의 축약 표현이다.

파워쉘을 실행하면 결과를 처리하고 바로 창이 닫힌다. 3행의 pause로 엔터 키를 입력하기 전까지 창이 닫히지 않게 된다. 이후의 코드에서는 별도로 표기하지 않으니 언급이 없으면 코드 마지막 행에는 pause가 있다고 생각하자.

파일을 더블 클릭으로 열면 설정에 따라 메모장으로 열릴 수도 있다. 파일에서 오른쪽 클릭 후 **PowerShell에서 실행 (Run with PowerShell)**을 클릭한다. 이 저장한 파일을 실행하면 다음처럼 출력되는 것을 확인할 수 있다.

파워쉘 명령행에서 실행했을 때 오류가 발생하면 이 포스트의 문제 해결 부분을 참고한다.

HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Expose-Headers: Content-Length,Content-Type,Date,Server,Connection
Strict-Transport-Security: max-age=31536000; includeSubdomains
Content-Length: 98
Content-Type: application/json
Date: Fri, 08 Jul 2016 09:50:00 GMT
Server: nginx/1.10.0

{"ok":true,"result":{"id":2775591989,"first_name":"webscrapbot","username":"haruair_webscrap_bot"}}

Press Enter to continue...:

응답 헤더와 json 형식의 응답 내용을 확인할 수 있다. pause로 인해 창이 닫히지 않고 엔터를 입력하면 그제서야 닫힌다.

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot<API 토큰>/getMe"
echo $info.ok

이 코드에서는 앞과 다르게 Invoke-RestMethod 라는 함수를 확인할 수 있다. Invoke-RestMethod cmdlet은 REST 웹서비스를 호출하고 그 반환된 결과를 구조화된 데이터로 사용할 수 있는 기능을 제공한다. 별도의 라이브러리 없이 간편하게 REST 호출이 가능할 정도로 기본적으로 제공하는 기능이 다양하다.

앞에서 확인한 코드에서는 HtmlWebResponseObject를 반환해서 RawContent 프로퍼티로 접근할 수 있었던 반면에 Invoke-RestMethodPSCustomObject를 반환한다. 그래서 JSON 개체를 쉽게 프로퍼티처럼 접근해서 사용할 수 있기 때문에 편리하다. 2행의 $info.ok는 앞에서 확인했던 응답 내용과 같이 true를 반환한다.

앞 응답 내용에서 first_name, username처럼 result 내에 있는 데이터는 어떻게 확인할 수 있을까? 다음과 같은 코드를 추가하면 쉽게 확인할 수 있다.

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot<API 토큰>/getMe"

if ($info.ok) {
    echo $info.result | Format-List
}

$info.ok가 참이면 $info.result의 내용을 출력한다. 여기서 | 기호를 사용해서 Format-List를 추가했다. 코드를 실행하면 다음과 같은 결과를 확인할 수 있다.

id         : 2775591989
first_name : webscrapbot
username   : haruair_webscrap_bot

| 기호 즉, 파이프라인(pipeline)은 다른 쉘에서도 쉽게 볼 수 있는 기능으로 파워쉘도 동일하게 지원한다. 이 파이프라인을 사용하면 순서대로 앞 결과를 뒤 명령에서 입력으로 사용한다. 위 코드에서는 $info.resultFormat-List의 입력으로 사용하게 된다. 파워쉘에서는 결과를 보기 쉽게 목록으로 변환하는 Format-List를 제공한다. 이 cmdlet도 fl로 줄여 쓸 수 있다. 비슷하게 Format-Table은 표로 변환하며 특정 양식으로 변환할 수 있는 Format-Custom도 있다.

지금까지 텔레그램에 등록한 봇을 API로 접근할 수 있다는 점을 확인했다. 이제 텔레그램에 메시지를 전송하기 전에 사용자의 chatId를 알아내야 한다. 이 chatId를 알아내려면 어떻게 해야 할까? 봇이 받은 메시지를 확인하면 그 메시지를 보낸 사용자의 chatId를 찾을 수 있다. 받은 메시지는 getUpdates 메서드를 사용해서 확인 가능하다. 다음 코드를 추가해보자.

$updates = Invoke-RestMethod -Uri "https://api.telegram.org/bot<API 토큰>/getUpdates"

if ($updates.ok) {
    $updates.result | Select-Object -expandProperty message | Select-Object -expandProperty from text | Format-Table
}

앞에서 본 내용과 거의 비슷하다. Select-Object cmdlet은 개체 또는 개체의 프로퍼티에서 필요한 부분만 선택하는 기능을 제공한다. 이 cmdlet과 함께 사용한 -expandProperty 매개 변수는 특정 프로퍼티의 내용을 확장해서 데이터를 처리할 수 있게 한다. 각 파이프 별로 잘라서 실행해보면 그 변화를 확인할 수 있다. 이처럼 파이프라인은 여러 결과를 이어서 사용할 수 있으며 마지막 cmdlet으로 Format-Table을 사용해 표 형식으로 출력했다.

앞과 다르게 echo를 입력하지 않았다. echo 없이 작성해도 넣고 작성한 것과 동일한 결과가 나온다.

이제 이 코드를 실행해보면 빈 표가 출력되거나 에러가 발생한다. 그 이유는 봇과 텔레그램을 통해 메시지를 전송한 적이 없기 때문이다. 스팸 문제 때문인지 이 chatId는 실제로 해당 봇과 대화를 한 적이 있는 경우에만 얻을 수 있다. 텔레그램 봇 생성하기에서 받은 봇 링크(http://telegram.me/<생성한 Bot ID>)를 클릭한 다음에 /start 또는 아무 내용이나 메시지를 작성해서 전송한다. 전송한 다음에 스크립트를 실행하면 다음과 같은 결과를 확인할 수 있을 것이다.

       id first_name last_name username  text  
       -- ---------- --------- --------  ----  
138389563 Edward     Kim       haruair   /start
138389563 Edward     Kim       haruair   Hello

결과에서 확인할 수 있는 것처럼 내 chatId는 138389563이다. 이 chatId를 사용하면 봇이 나에게 메시지를 전송하게끔 할 수 있다.

지금까지 작성한 status.ps1을 정리하면 다음과 같다.

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getMe"

if ($info.ok) {
    echo $info.result | Format-List
}

$updates = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getUpdates"

if ($updates.ok) {
    $updates.result | Select-Object -expandProperty message | Select-Object -expandProperty from text | Format-Table
}
pause

설정 파일 분리하기

Bot API를 호출할 때 사용하는 토큰과 chatId는 별도의 설정 파일로 분리하면 깔끔하게 사용할 수 있다. config.json 파일을 생성하고 다음처럼 내용을 작성한다. 각 항목은 내용에 맞게 입력한다.

{
    "token": "<API 토큰>",
    "chatId": "<메시지를 받을 chat_id>" 
}

이 json 파일을 사용하도록 status.ps1을 다음 코드를 추가한다.

$config = Get-Content .\config.json | ConvertFrom-Json

Get-Content cmdlet은 파일 내용을 불러온 후에 ConvertFrom-Json cmdlet에게 그 내용을 전달한다. ConvertFrom-Json는 JSON을 개체로 변환해서 $config에 저장한다.

이제 토큰은 $config.token 식으로 접근해서 사용할 수 있다. 이제 코드를 수정해보자. 파워쉘의 문자열 내에서는 $(변수) 형식으로 문자열 보간(String Interpolation)이 가능하다. 여기까지 진행한 status.ps1은 다음과 같다.

$config = Get-Content .\config.json | ConvertFrom-Json

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getMe"

if ($info.ok) {
    echo $info.result | Format-List
}

$updates = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getUpdates"

if ($updates.ok) {
    $updates.result | Select-Object -expandProperty message | Select-Object -expandProperty from text | Format-Table
}

pause

Get-Content를 사용할 때 주의해야 하는 점은 기본 인코딩이 따로 지정되어 있지 않다는 부분이다. 인코딩을 지정하지 않아도 큰 문제가 되지 않는 범위의 내용을 저장한다면 문제가 없지만 한글은 제대로 불러오지 못한다. 그래서 -encoding 매개 변수를 이용해서 utf8로 불러오면 문제 없이 불러올 수 있게 된다. 다음 코드를 참고하자.

$config = Get-Content .\config.json -Encoding utf8 | ConvertFrom-Json

웹페이지 호출한 다음 결과물 가공하기

이제 본격적으로 메시지를 통해 보낼 데이터를 가공하려고 한다. message.ps1을 생성해서 다음 내용을 작성한다.

$haruair = Invoke-WebRequest -Uri "http://haruair.com/blog/"
$titles = $haruair.ParsedHtml.getElementsByTagName("h2") | Where-Object { $_.className -eq "entry-title" }
$links = $titles.getElementsByTagName("a") | Select-Object innerText, href

1행에서는 http://haruair.com/blog/ 페이지의 내용을 Invoke-WebRequest cmdlet으로 가져왔다. 이 페이지에는 여러 포스트가 한번에 출력된 목록 페이지로 각각의 제목과 링크를 가져와서 메시지를 보내는데 활용하려고 한다.

2행은 ParsedHtml 프로퍼티에 접근한 다음에 getElementsByTagName 메서드를 사용해서 포스트 제목에 해당하는 엘리먼트인 <h2>를 선택했다. 그 다음에 파이프라인을 이용해서 Where-Object cmdlet으로 넘겼고 html 엘리먼트의 클래스명을 기준으로 필요한 제목만 선택했다. Where-Object는 개체 컬렉션에서 특정 프로퍼티의 값을 사용해 개체를 선택할 수 있는 cmdlet이다. Where 또는 ?로 표기할 수 있다.

이제 제목 엘리먼트에서 제목과 해당 포스트로 이동할 링크를 찾을 차례다. 3행을 보면 선택한 <h2> 엘리먼트에서 <a>를 다시 선택한 후에 앞에서 봤던 Select-Object를 활용해서 엘리먼트 내용과 링크만 선택한 다음 $links에 반환했다.

텔레그램 봇 API로 메시지 전송하기

이제 텔레그램으로 메시지를 전송하려고 한다. message.ps1에 다음 내용을 추가한다.

foreach ($link in $links) {
    $message = "New Post! $($link.innerText) $($link.href)"
    $encodedMessage = [System.Web.HttpUtility]::UrlEncode($message)
    $info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/sendMessage?text=$encodedMessage&chat_id=$($config.chatId)"
}

$linksforeach 문으로 반복했다. $message에 전송할 메시지를 작성했다.

3행에서는 지금까지 보지 못했던 특이한 문법이 존재한다. System.Web.HttpUtility는 .Net Framework에 포함된 클래스로 URL을 인코딩 또는 디코딩 하는 메서드를 제공한다. 파워쉘에서는 .Net의 클래스와 메서드를 직접적으로 사용할 수 있다. 여기서는 [System.Web.HttpUtility]::UrlEncode를 사용해서 메시지를 인코딩했다.

4행은 API의 sendMessage 메서드를 사용해서 메시지와 메시지를 받을 chat_id를 전달했다.

지금까지 작성한 message.ps1을 종합하면 아래 코드와 같다.

$config = Get-Content .\config.json -Encoding utf8 | ConvertFrom-Json

$haruair = Invoke-WebRequest -Uri "http://haruair.com/blog/"
$titles = $haruair.ParsedHtml.getElementsByTagName("h2") | Where-Object { $_.className -eq "entry-title" }
$links = $titles.getElementsByTagName("a") | Select-Object innerText, href

foreach ($link in $links) {
    $message = "New Post! $($link.innerText) $($link.href)"
    $encodedMessage = [System.Web.HttpUtility]::UrlEncode($message)
    $info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/sendMessage?text=$encodedMessage&chat_id=$($config.chatId)"
}

이 파일을 실행하면 메시지가 잘 전달되는 것을 확인할 수 있다. 하지만 스크립트를 실행 할 때마다 글이 전송된다. 이전에 전송한 글은 전송하지 않게 하려면 어떻게 해야 할까?

메시지 비교하고 csv로 저장하기

파워쉘은 Import-CsvExport-Csv cmdlet으로 손쉽게 CSV를 다룰 수 있다. 또한 개체를 비교하는 Compare-Object cmdlet을 사용해서 항목을 비교할 수 있고 SideIndicator 프로퍼티로 비교 결과를 확인할 수 있다.

Import-Csv cmdlet이 존재하지 않는 파일을 불러오려고 할 때는 오류가 발생한다. 파워쉘도 try catch finally문법을 지원한다. 코드에서는 .Net Framework의 _System.IO.FileNotFoundException_로 catch하는 것을 확인할 수 있다.

아래는 최종적인 message.ps1 코드 내용이다.

$config = Get-Content .\config.json -raw -encoding utf8 | ConvertFrom-Json

$haruair = Invoke-WebRequest -Uri "http://haruair.com/blog/"
$titles = $haruair.ParsedHtml.getElementsByTagName("h2") | Where-Object { $_.className -eq "entry-title" }
$links = $titles.getElementsByTagName("a") | Select-Object innerText, href

try {
  $oldLinks = Import-Csv .\data.csv
}
catch [System.IO.FileNotFoundException] {
  $oldLinks = @()
}

$diff = Compare-Object $oldLinks $links -property innerText, href | Where-Object { $_.SideIndicator -eq '=>' }
$measure = $diff | Measure-Object

if ($measure.Count -ne 0) {
  foreach ($link in $diff) {
    $message = "New Post! $($link.innerText) $($link.href)"
    $encodedMessage = [System.Web.HttpUtility]::UrlEncode($message)
    $info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/sendMessage?text=$encodedMessage&chat_id=$($config.chatId)"
  }

  $links | Export-Csv .\data.csv -Encoding utf8
}

Measure-Object는 개체의 수를 셀 때 사용한다. measure로 줄여서 사용할 수 있다. 위 코드에서는 다소 장황하게 사용했는데 다른 방식으로 작성하는 방법도 존재한다. 아래 코드처럼 ForEach-Object cmdlet도 사용할 수 있다. ForEach-Object의 축약은 %로도 줄여서 사용 가능하다. 가장 간단한 방법은 괄호를 이용한 마지막 방법이다.

$measure = $diff | Measure-Object
if ($measure.Count -ne 0) {
    // do something
}

if (($diff | Measure-Object | ForEach-Object { $_.Count }) -ne 0) {
    // do something
}

if (($diff | measure | % { $_.Count }) -ne 0) {
    // do something
}

if (($diff | measure).Count -ne 0) {
    // do something
}

이제 message.ps1을 실행하면 data.csv에 파일이 생성되고 새 글이 올라오지 않는 경우에는 메시지를 전송하지 않는다. data.csv를 열어보면 개체 목록이 csv로 변환된 내용을 확인할 수 있다. 모든 과정이 끝났다. 필요에 따라서 message.ps1을 Windows 작업 스케줄러를 사용해 반복적으로 호출하면 최신의 업데이트 상황을 수시로 확인해서 메시지로 전송받을 수 있다.

위 스크린샷에서 메시지를 전송 받은 결과를 볼 수 있다.


문제 해결

스크립트를 작성하고 구동하는 과정에서 겪은 문제를 정리했다.

실행 정책 ExecutionPolicy

파워쉘 스크립트를 파워쉘 명령행에서 실행하면 다음과 같은 에러가 출력된다.

.\status.ps1 : File W:\path\to\status.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at 
http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ .\message.ps1
+ ~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

서명되지 않은 스크립트 파일은 보안상 기본적으로 실행할 수 없다. 현재 실행 정책은 Get-ExecutionPolicy로 확인할 수 있고 Set-ExcutionPolicy cmdlet으로 조정할 수 있다. 기본 정책은 _Restricted_인데 다음 명령을 사용하면 정책 수준을 낮출 수 있다.

Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser

테스트를 모두 완료한 다음에는 보안을 위해 다시 원래대로 돌려놓도록 하자.

Set-ExecutionPolicy -ExecutionPolicy Restricted -Scope CurrentUser

높은 정책 수준에서도 실행 가능하게 하려면 스크립트에 서명을 하는 방법이 있고 제한적인 용도라면 직접 서명(Self-Signing)을 하는 것도 가능하다.

Invoke-WebRequest 오류

Invoke-WebRequest은 Windows에 내장된 Internet Explorer를 내부적으로 참조한다. 그래서 Internet Explorer를 단 한 번도 실행한 적이 없다면 다음과 같은 메시지가 출력된다.

Invoke-WebRequest : The response content cannot be parsed because the Internet Explorer engine is not available, or Internet Explorer's first-launch configuration is not complete. Specify the UseBasicParsing 
parameter and try again. 
At W:\path\to\status.ps1:4 char:6
+ $response = Invoke-WebRequest -Uri "https://api.telegram.org/bot$($config.token ...
+      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotImplemented: (:) [Invoke-WebRequest], NotSupportedException
    + FullyQualifiedErrorId : WebCmdletIEDomNotSupportedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

IE를 한 번 켰다 끄면 해결 된다.


간단하게 텔레그램을 통해 메시지를 전송하는 스크립트를 파워쉘에서 작성했다. 여기서 다룬 파워쉘의 기능도 극히 일부에 불과하다. 2006년에 출시해서 무려 9년 넘는 기간 동안 성숙해 온 파워쉘은 타입 지원, 클래스 등 쉘 스크립트 답지 않게 폭넓은 기능을 제공한다. 더 자세히 보고 싶다면 MSDN PowerShell에 있는 글이 많은 도움이 될 것이다. 윈도 환경에서 개발을 한다면 꼭 파워쉘을 살펴보도록 하자. 😀

  • Mono 기반의 구현인 Pash도 있다. 조만간 dotnet core 기반의 구현도 나오지 않을까 기대된다. 
  • 윈도 머신이 잠시 필요해서 lubuntu를 설치한 컴퓨터를 다시 Windows로 복구했다. Lubuntu로 밀기 전에 dd 명령으로 이미지를 백업해뒀는데 과정을 따로 기록해두질 않아서 삽질을 좀 하게 되었다. 그래서 이번에는 안전하게 잊을 수 있도록 기록을 남긴다.

    윈도 이미지 만들면서도 이게 괜찮은 방법인지는 반신반의 했는데 일단 복원까지 하는데 성공했으니 별 문제는 없는 것 같다. 일단 내 노트북은 32GB 밖에 되지 않아서 저장용도 usb stick을 사용했다.

    준비물

    • 부팅용 USB
    • 이미지 저장 및 복원용 USB

    준비 과정

    준비 작업은 맥에서 진행했다. 이미지 저장 및 복원용 USB는 exFat으로 포맷한다. 맥과 리눅스에서 모두 사용할 수 있는 형식이고 용량 문제도 없다. Disk Utility 에서 포맷할 수 있다.

    남은 작업을 편하게 하기 위해 ubuntu live usb를 만든다. 여기서는 설치에 사용했던 lubuntu 이미지를 사용했다.

    $ hdiutil convert -format UDRW -o /path/to/lubuntu-15.10-desktop-amd64 lubuntu-15.10-desktop-amd64.iso
    

    이렇게 변환하면 lubuntu-15.10-desktop-amd64.dmg 파일이 생성된다. 이제 live usb를 연결하고 해당 usb의 경로를 다음 명령으로 확인한다.

    $ diskutil list
    

    출력 결과에서 용량이나 명칭으로 드라이버를 확인한다. 만약 마운트가 되어 있는 상태라면 diskutil unmountDisk /dev/diskN으로 마운트를 해제한다.

    $ diskutil unmountDisk /dev/disk3
    

    이제 usb에 이미지를 덮어 쓴다.

    $ sudo dd if=/path/to/lubuntu-15.10-desktop-amd64.dmg of=/dev/rdisk3 bs=1m
    

    입력이 모두 완료되면 맥에서 드라이브를 읽을 수 없다는 오류가 뜨는데 정상이며 Eject를 누르면 과정이 끝난다.

    백업 및 복원 과정

    이제 usb를 사용해서 부팅을 한다. usb로 부팅이 되지 않는다면 BIOS에서 부팅 순위를 확인한다. USB를 꼽고 부팅을 하면 Try Lubuntu 메시지를 선택해서 lubuntu를 켠다.

    마우스와 키보드가 잘 작동하면 다음 단계로 넘어가면 된다. 블루투스 마우스의 경우에는 Win + R 키로 실행 창을 띄운 후, blueman-manager를 입력해서 실행한다. 그러면 관리자 창이 뜨는데 탭 키와 방향키를 잘 사용해서 마우스를 연결하면 된다.

    마우스가 잘 되면 네트워크 관리자에서 wifi를 연결한다. 시작 상태 막대 끝에 아이콘을 활용한다. 그리고 exfat을 사용할 수 있도록 패키지를 설치한다.

    $ sudo apt-get install exfat-utils exfat-fuse
    

    백업할 드라이브 경로를 디스크 목록에서 찾는다.

    $ sudo fdisk -l
    

    이제 컴퓨터에 이미지 저장용 USB를 꼽는다. 앞서 dd와 같이 백업할 드라이브를 지정하고 저장할 경로를 지정하면 이미지로 백업하게 된다. 현재 저장되어 있는 OS가 무엇인지 상관없이 전체를 백업하게 된다.

    $ sudo dd if=/dev/sdb of=/media/lubuntu/backup/backup.img bs=1m
    

    복원은 반대로 하면 된다. 대신, 이미지를 덮기 전에 마운트 해제가 정상적으로 되어 있는지 확인한다. 숫자를 착각하거나 경로를 잘못 읽어서 엉뚱한 드라이버를 덮지 않도록 조심한다.

    $ sudo mount # 명령으로 마운트를 확인, 마운트되어 있다면 아래 명령으로 언마운트
    $ sudo umount /media/lubuntu/main
    $ sudo dd if=/media/lubuntu/backup/backup.img of=/dev/sdb bs=1m
    

    드라이브를 통채로 떴기 때문에 MBP나 UEFI 같은건 따로 안잡아줘도 되는 것 같다.

    진행 상황 확인하기

    dd 진행 상황을 확인하기 위해서는 status 플래그를 넣으면 된다는데 dd 버전마다 달라서 되는 경우도 있고 안되는 경우도 있다. pv를 설치하는 방법이 간편하다.

    $ sudo apt-get install pv
    $ sudo -i # 귀찮으니 root로 전환
    $ dd if=/media/lubuntu/backup/backup.img | pv | dd of=/dev/sdb bs=1m
    

    2012년 3월, 덜컥 호주행 비행기를 타고 멜버른에 도착한 그 날이 아직도 생생하다. 캐리어를 밀고 백팩커에 체크인 하던 나를 기억해보면 그 때의 나는 무슨 생각으로 이런 일을 저질렀을까, 그런 생각이 든다. 그렇게 호주 생활을 시작한지 만 4년이 조금 넘은 지금, 2016년 6월, 호주 영주권을 갖게 되었다.

    겁 없이 올 수 있던 이유

    당시에는 엄청 준비하고 나왔다고 생각했지만 지금 봐서는 참 도전적이었고 나쁘게 말하면 많이 안일하게 준비했다. 영어도 부족했고 현지 사정에도 밝지 않았다. 현지 취업 사이트에서 취업 공고 보고 내 이력서와 맞는 자리가 얼마나 있을지 알아본 정도였다. 그래서 4년이란 불안정한 기간동안 마음 고생도 심했었다. 즐길 수 있던 시간도 있었지만 그만큼 자기 관리가 필요했다. 타지에서 오래 지낸 경험이 없어 내게 쉬운 일은 아니였다. 감사하게도 여기에서 만난 분들의 도움을 많이 받을 수 있었다.

    지금은 탈조선에 성공한 것을 축하한다는 인사도 받긴 하지만 내가 나올 즈음인 4년 전까지만 해도 이토록 탈조선에 대한 담론이 많지 않았다. 게다가 내 삶에 직접적인 영향을 준 한국 경험의 시간은 4년 전을 기준으로 멈춰 있다. 간접적으로 듣는 경제 사정이나 청년 계층의 취업난은 실제가 어느 정도 수준인지 가늠하기 어려울 정도로 심각하게 느껴진다. 나는 엄청난 의지로 꼭 탈출하고 말리라 정도로 생각하고 나온 것이 아니라서 요즘 해외로 취업하고 싶은데 어떻게 하면 되냐는 질문에 다소 다른 온도의 답변을 하게 되는 것 같다.

    내가 다니던 대학 학비는 학기에 170만원 정도 하는 지방 국립대였고 집에서 살았기 때문에 자취를 할 일도 없었다. 군 전역 후에 1년 간 회사를 다니며 모은 적금을 정리하고 호주 올 준비를 한 다음에 300만원을 환전해서 호주로 넘어왔다. 부양 가족도, 갚아야 하는 학자금 대출도 없었다. 만약 학교 학비가 더 비쌌더라면 앞에 낸 비용이 아까워서라도 학교를 끝내야 한다는 강박이 생기지 않았을까 생각도 든다. 워킹홀리데이는 그나마 가장 적은 밑천으로 시작할 수 있는 선택지다. 하지만 이런 말을 어디서 쉽게 하지 않았던 이유는 이런 내 배경으로 쉽게 느끼는 것은 아닌가 고민했기 때문이다. 다녀야 할 학교도 있고 학자금 대출까지 있다면 워홀 커뮤니티에서 흔히 하는 말처럼 경험, 영어, 돈 중 하나 내지 둘 챙기는 것 외에는 결정을 내리기 쉽지 않을 것 같다. 물론 돈이 더 있다면 더 좋은 선택지가 많겠지만 말이다.

    지금 보면 참 어린 생각이지만 프로그래밍으로 취업할 수 있을거란 자신감이 있었다. 어릴 때부터 재미로 만들던 웹사이트인데 이 일로 돈을 번다는 것을 군 입대 전에야 알았다. 전역하고 나서는 지방 기업이지만 웹에이전시에서 개발팀장으로 근무하면서 한참 자신감이 충만해졌다. 같은 일을 한다면 전혀 두려울 것이 없을 정도로 알게 되니 겁이 없어졌다. 한국 이 촌구석에도 이렇게 일이 있는데 호주라고 없겠나 싶은 생각도 들었다.

    오기 전에도, 오고 나서도 흔하지 않은 케이스였고 좋은 이야기를 들은 기억이 손에 꼽았다. 그래서 지금까지 지낼 수 있었던 점에 더 감사하게 된다.

    호주에 도착해서

    지인이 도와줘서 미리 만들어온 커버레터와 준비해온 이력서를 들고 지원할 수 있는 모든 포지션에 지원했다. 열린 포지션이라면 어디든지 지원 했지만 리쿠르터를 거치는 경우에는 아무래도 영어가 약해서 면접까지 가는 일이 쉽지 않았다. 이런 일은 예상했지만 실제로 경험을 하고 반복하다보면 기운빠지는 일이긴 하다. 그래도 주변에서는 리쿠르터가 아예 전화를 하지 않는 경우도 많다고 그래서 전화 오고 인터뷰가 잡힌다는 사실 자체에 감사했다. 많은 전화 덕분에 오히려 더 빠르게 전화에 익숙해질 수 있지 않았나 생각이 든다.

    그러던 중에 급한 프로젝트에 투입된 적도 있었다. 2주 정도의 짧은 기간이었지만 좋은 레퍼런스를 얻을 수 있었다. 이 레퍼런스를 가지고 부지런히 자리를 찾으려고 노력했다. 이때 만난 저스틴님께도 조언을 많이 들을 수 있었고 지금까지도 항상 좋은 말씀을 해주셔서 감사할 따름이다.

    리쿠르터를 통과하는 일이 아무래도 적다보니 콜드메일도 정말 많이 보냈다. 멜번 지역에 있는 회사를 구글 맵스에서 검색해서 일일이 웹사이트를 확인했다. 채용 페이지가 없을 때는 문의 이메일로 이력서와 커버레터를 보내기도 했다. 인터뷰도 더 많이 잡을 수 있었고 그렇게 지금 회사에 다니게 되었다.

    호주에 지내는 내내 호주에서 아무 일도 못해보고 한국을 돌아갔다면 어땠을까 하는 궁금증은 늘 따라다녔다. 제주에서 임용고시를 준비하며 씨름하고 있었을까, 서울에서 계속 웹개발을 하고 있었을까.

    비자 문제

    해외 체류에 있어서 비자는 늘 문제다. 물론 학력이 좋거나 많은 연봉을 받는 사람이라면 비자는 전혀 문제가 되지 않는다. 모셔가야 하는 사람 발목은 안잡는다. 그 외의 경우는 내 자신이 왜 비자를 받아아 하는지 설득해야 한다. 작게 보면 회사의 상사에게 그래야 하고 크게 보면 호주 정부에게 내 비자의 타당성을 서류로 검증받아야 하는 것이다. 이렇게 말은 쉽지만 체류하는 사람마다 비자와 관련한 에피소드는 다들 하나는 갖고 있을 정도다.

    비자 발급 비용도 적지 않다. 나는 고맙게도 모든 비용을 회사에서 처리해줘 부담없이 잘 받을 수 있었다. 준비해야 할 서류가 크게 복잡하게 느껴지지 않아 대리인을 고용하지 않았다. 그리고 호주 이민성에서 제공하는 체크리스트를 보며 준비 서류를 챙겼다. 그렇게 워킹홀리데이 비자에서 Subclass 457 비자로 전환 후 2년 반을 지내고 영주 비자인 ENS 비자를 신청했다.

    비자를 신청하기 전에 요건을 충족하는 것이 1차 관문이고 비자를 신청하고 처리되기까지 기다리는 일이 2차, 담당자가 배정되어 통과되는 일이 마지막 관문이다. 처리를 기다리는 기간이 기본적으로 5개월 이상인데 이 기간 동안 별 생각이 다 든다. 게다가 까탈스러운 담당자를 만난다면 정말 정신을 붙잡기가 쉽지 않다. 내 경우에도 꼼꼼한 담당자를 만나서 번거로운 일이 있었지만 다행히 큰 문제가 되진 않아 감사했던 기억이 난다.

    영주권은 끝이 아니라 시작

    마치 고등학생 때 수능 보고 대학만 가면 모든 일이 끝날 것 같지만 더 많은 선택지와 삶의 방향 앞에서 혼란을 겪는 것과 비슷한 것 같다. 영주권을 받고 나서 기쁜 마음도 분명 있지만 이제는 더 이상 내 비자 상태를 탓하며 아무 일도 안하며 손놓고 있을 수 없다는 생각이 들어 괜스레 부담감도 생긴다. 그런 부담감도 있고 앞으로 어떤 일을 해야 할 지 뚜렷하게 생각해보지 않고 당장 앞에 있는 일에만 바쁘게 지냈는데 막상 그 날이 와서 어떻게 해야 할지 잘 모르겠다고 할까. 그래도 기쁨과 감사함으로 앞으로 시간을 잘 준비해야겠다는 생각을 한다.

    호주에서의 삶부터 지금까지 지내온 시간은 나에게 너무나도 새로운 경험이었고 평생 가지고 갈 내 인생의 밑천이 되었다. 호주에서 어떤 일이 있었든지 또 다시 다른 나라를 알아보고 도전해보지 않았을까. 지금 내가 갖고 있는 모든 것을 털고 또 새롭게 시작해보겠냐 그러면 그럴꺼라 대답할 것이다. 그 인생의 충격은 또 다시 경험해보고 싶을 정도로 매력적이다.

    다행인지 삶은 여기에 끝이 아니라 도전의 연속이다. 영주권이 큰 고비라고 생각했지만 앞으로도 등정하고 싶은 산도, 올라야 하는 산도 많이 보인다. 그러면서도 여전히 내 자신에게서 부족한 부분을 많이 보게 된다. 더 나아지기 위해 노력하는 동시에 결여를 부정하거나 괴로워하거나 덮어두지 말고, 있는 그대로를 자연스럽게 받아드릴 수 있는 사람이 되었으면 좋겠다. 그리고 지금까지의 삶에서 갚아야 할 감사가 더 많아졌다. 부지런히 살고 열심히 지내서 도움 주신 모든 분들에게 보답하고 싶고 다른 사람에게 그런 도움을 줄 수 있는 사람이 되도록 노력해야겠다.

    앞으로 어떤 삶을 마주하게 될 지 기대된다.


    다음은 호주 생활과 관련해서 썼던 글이다.

    색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.