5월 052017
 

USB 유선랜 어뎁터를 구매하게된 것은, 집에 있는 공유기가 802.11n 밖에 지원하지 았고 주변의 네트워크 환경 때문인지 대역폭이 널뛰기 하는 현상에다가 무엇보다도 내부 데이터 서버로 데이터 백업시 느린 대역폭으로 너무 많은 시간이 소모되는 문제가 있었기 때문이다. 공유기를 업그레이드해서 해결하는 방법도 있겠지만 어차피 노트북과 공유기 사이의 거리가 바로 옆에 붙어 있을 정도로 가까웠기 때문에 외부환경에 강한 유선랜으로 연결하기로 하였다. 이 경우 필자의 맥북프로에서는 썬더볼트 이더넷 어뎁터를 사용하면 되는데 안타깝게도 듀얼 모티터를 사용중이기 때문에 2개 있는 썬더볼트를 다 써버렸기 때문에 USB쪽으로 눈을 돌려야 했다. 그래서 여러 USB유선랜 어뎁터를 찾아보던 도중, USB포트를 제공하는 어뎁터가 있다는 것을 알게 되어 구매하게 되었다. 맥북프로레티나 2012년도 모델은 USB포트가 2개밖에 없기 때문에 USB포트도 살리면서 유선랜도 추가적으로 쓸 수 있는 Moshi USB유선랜 어뎁터는 최적의 선택이였다.

USB3.0 to Gigabit Ethernet Adapter 포장 앞면

일반적인 케이블 포장이였고, 안의 내용물이 보이게 되어 있어서 USB3.0 소켓을 하나 제공하는 것을 겉만 보고도 알 수 있도록 해두었다.

Mosh USB 3.0 to Ethernet adapter 포장 뒷면

맥북 에어에서 인터넷을 연결하는 방법이라고 써있는데 옛날에 맥북에어에 썬더볼트 없을 때 나온 상품인 것 같다. 실제로는 윈도우, 맥, 리눅스에서 사용해도 문제 없다고 한다. 최대 1,000Mbps로 동작 가능하다는 이야기 적혀있었다.

Moshi 어뎁터와 애플 정품 썬더볼트용 기가 이더넷 어뎁터 크기 비교

정품 기가 이더넷 어뎁터에 비해서는 크기가 크지만 USB3.0을 사용하기 위한 소켓이 추가되어 있기 때문이라고 생각했다.

Moshi 어뎁터와 애플 정품 썬더볼트용 기가 이더넷 어뎁터 크기 비교(이더넷포트 쪽)

너비는 타이트하게 딱 맞는 정품 어뎁터에 비해서는 크다. 실제로 무게차이는 별로 안나게 느껴졌다. 제질은 Moshi쪽은 알루미늄 느낌이 나는 제질이였고, 애플제품은 플라스틱 제질의 느낌으로 차이가 있었다.

Moshi 어뎁터 – USB소켓 쪽 사진

Moshi 어뎁터의 소켓을 자세히 보면 색깔은 흰색이지만 사용설명서에 따르면 USB3.0이 지원되는 포트이다. 요즘 USB Type-C가 인기지만 USB Type-C를 사용하는 장비를 가지고 있는 것이 없어서 크게 의미는 없었다. 정 필요하면 miniUSB를 USB Type-C로 변환하는 케이블을 사서 쓰면 된다.

최종적으로 설치 결과

기존에 모니터로 연결되던 선과 무선랜어뎁터를 연결하고 Dell모니터의 USB허브로 연결되던 선을 Moshi 어뎁터에 연결했다. 그렇게 설치하니 깔끔하게 설치완료되었다. 물론 USB3.0은 5Gbps라는 속도 한계가 있긴 하지만 동시에 대역폭을 전부 소모해버릴 정도의 장비는 가지고 있지 않기 때문에 문제 없을 것이다.

과연 Moshi 어뎁터는 내가 만족할 수 있는 퍼포먼스를 제공해 주고 있을까?란 궁금증이 들어서 직접 테스트 해보기로 했다. 간단한 테스트라 정확히 측정된 것이 맞냐는 반론이 있을 수 있는데 대략적인 부분만 확인하기 위함이기 때문에 이해하고 넘어가도록 하자.

테스트 구성도

테스트 환경은 위와 같다. 끝판왕이라 불리우는 ASUS N54U 공유기와 테스트로 쓸 서버를 준비하였다. ASUS N54U는 1Gbps를 지원하는 스위치로 4개의 포트를 지원한다. 마찬가지로 MicroServer G7 서버도 1Gbps를 지원하며 Moshi 어뎁터도 마찬가지이다. 선들은 각각 Full Duplex 모드로 연결되어 있으며 Fibre Channel은 물론 On된 상태로 동작한다. Macbook Pro retina mid2012모델은 802.11n까지만 지원하지만 네트워크 카드 업그레이드를 통해 802.11ac를 사용 가능하도록 해두었다. 하지만 ASUS N54U의 한계로 802.11n으로 접속하여 테스트 하였다. 채널은 5GHz 대역을 이용하였다. 부하는 최대로 줄이기 위해 최대한 네트워크, CPU 등의 작업이 일어날 만한 요소들은 제거했으나 완전하게 제거할 수기에는 한계가 있었다.

테스트는 iperf 라는 툴을 이용하였다. 설치도 쉽고 간단히 테스트하기에 좋아 보였기 때문에 선택하였다.

우분투(16.04)의 경우 sudo apt-get install iperf 로 끝났고, 맥의 경우 brew install iperf 로 끝! 간편히 설치 가능하다. 서버에서는 iperf -s -p 5000 이란 명령어로 실행하면 되고, 클라이언트의 경우에는 iperf -c <<host ip address>> -p 5000 으로 실행 가능하다. 간단하므로 이 정도로 간략하게 설명하겠다.

결과를 보면 아래와 같다.

Moshi adapter 성능 측정

애플 정픔 기가비트 이더넷 어댑터

802.11n Wi-fi 성능 측정 결과

간단히 정리하면 아래와 같다.

  • 802.11n : 266Mbps   (최대 성능 300Mbps)
  • Moshi 어댑터 : 932Mbps (최대 성능 1000Mbps)
  • 애플 정품 어댑터 : 940Mbps (최대 성능 1000Mbps)

모든 영향을 끼칠 수 있는 요소를 제거하지 않은 테스트이기 때문에 코멘트를 하는 것이 조심스럽지만 모든 테스트에서 거의 최대 성능 가까이 속도가 나오는 것을 확인할 수 있었다. Moshi어뎁터가 성능이 아주 약간 적게 나왔지만, 여러번 실험하면 그 차이는 거의 없어진다. Moshi 어뎁터나 정품 어뎁터나 크게 네트워크 성능 차이가 발생하지는 않았다. 딱히 성능 때문에 불안해할 필요는 없어 보인다. 어차피 USB3.0이나 선더볼트나 데이지채인으로 연결하면 전체 대역폭을 나눠 쓰게 되는 건 마찬가지이다.

결과적으로 Moshi USB3.0 to Ethernet adapter를 선택한 것은 적절한 선택이였으며 속도도 거의 최대 성능에 가깝게 나왔다. 기존에 무선으로 데이터 전송할 때 보다, 실제로 3배이상 빨라져서 내부 서버 사이에서 데이터를 주고 받는 시간을 절약할 수 있게 되어서 매우 고무적이었다. 만약 집에서 유선랜을 아직 쓰고계신 분이 있고, 썬더볼트 포트가 부족한 경우에 USB3.0을 사용 중이라면 매우 좋은 선택이 될 것이라고 확신한다.

 

4월 172017
 

이번에 구입해 본 것은 펠리칸의 베스트 셀러 모델들 중 하나인 M600 만년필이다. 교보핫트랙스를 통하여 구매하였으며 Gmarket에서 저렴하게 구매 가능하였다. 재미있는 것은 Gmarket에 입점한 HMall을 통해서 구매 했고, HMall에 핫트랙스가 입점해 있어서 두 지점의 할인을 다 받을 수 있어서 였던 것 같다.

M400은 고시용 만년필로도 많이 알려져 있으며, M100이나 M200같은 경우에는 입문용으로도 많이 사용하고 있는 듯 하다. 참고로 M600은 M400보다 크기가 더 큰 모델이다. 뒷 숫자가 5로 끝나는 M405, M605 같은 모델들도 있는데 이는 은장 모델을 의미 한다. 기본적으로는 금장 모델이지만 취향에 따라서 알맞게 구입하면 되겠다. M800이상은 플래그쉽 모델로 가격도 꽤 비싸다.
M600을 선택하게 된 것은 M400의 경우 두깨가 너무 작아서 손크기에 따라서 알맞지 않다는 의견들을 보고 수용한 결과이다. 그렇다고 M800을 사기에는 총알이 부족했기 떄문에 적당선에서 타협하였다. M600은 14k의 금촉을 사용한 모델이고, 만약 한정판을 구매하지 않는 다는 전제하에 흑색, 녹색, 청색, 적색 중에서 고를 수 있다. 글씨 두깨와 같은 경우에는 제일 세필인 EF부터 여러가지가 있는데 좁은 공간에다가 많은 것을 표현해야하는 동양권(한자, 일본어, 한글)에서는 최대한 얇은 것이 좋다고 한다.
펠리칸의 문제점은 유명한 커뮤니티를 보면 “참수칸”이라는 악명을 가지고 있기도 하다. 요는 슬릿(펜촉 끝 부분)의 분할이 5:5가 아니라 6:4 라거나 심지어 8:2 라거나, 직각 분할이 아니라 사선 분할이라던지, M800과 같은 플래그쉽 모델도 저런 문제가 발생하는 QC문제가 큰 듯 하다. 그래서 오프라인에서 구매하는 것을 추천한다고 하였으나 온라인 구매가가 너무 저렴하여 그냥 온라인을 통해 구입하게 되었다. 저런 단점에도 불구하고 펠리칸 공식 수입업체인 신한커머스는 개념있는 AS로 유명하다고 한다.

핫트랙스에서 구매하면 검정색 잉크를 공짜로 줘서 받자마자 바로 잉크를 충전해서 써볼 수 있었다. 케이스도 여러가지 종류가 있다고 하는데 내가 받은 것은 일반적인 길죽한 박스 형태였다.

펠리칸 만년필 박스

박스를 열면 펠리칸이라고 써있는 로고가 나를 맞이한다.

펠리칸 만년필과 잉크 박스

뚜껑을 열면 펜과 보증서 그리고 사용설명서가 들어 있다. 보증서를 보면 언제까지 AS를 받을 수 있는지 여부와 이 것이 정품임을 확인하는 내용이 적혀 있다. 펜은 흰색의 가죽제질의 포장제 안에 들어 있다. 리본 모양으로 묶어져 있어서 옛날에 종이를 말아논 두루마리를 연상하게 했다.

펠리칸 만년필과 보증서

리본을 풀어서 펜을 꺼내면 비닐포장 안에 펜이 들어 있다. 먼지 유입을 차단하기 위해서겠지. 하지만 한편으로는 오프라인에서 샀으면 시필할 때 문제가 없을까라는 생각이 들었다.

펠리칸 만년필

비닐포장을 뜯어서 펜을 꺼내보니 생각보다 너무 가벼워서 놀랐다.

펠리칸 M600 만년필

위에서 보니 펠리칸 로고가 금으로 되어 있었는데, 알아보니 최신모델이라고 한다. 기존에는 금이 아니었다는 이야기를 들었다.

펠리칸 M600 캡

클립은 펠리칸의 부리모양을 형상하는 듯 했다. 펠리칸이라는 아이덴티티를 보여주고 있었다.

펠리칸 M600 캡

크기를 비교해 보면 모나미 볼펜보다는 짧지만 두깨는 훨씬 두꺼운 것을 확인할 수 있다. 손이 작은 사람의 경우 M400이 더 어울리는 선택이 될 수도 있을 것 같다. 물론 취향에 따라 다르니 오프라인 매장에 가서 시필해보는 것도 좋겠다.

펠리칸 만년필과 크기 비교

뚜껑을 열어서 비교해보면 아래와 같다.

캡을 벗긴 상태에서 만년필 크기 비교

뚜껑을 열 때 트위스트 방식이라서(돌려서 여는 방식) 기존에 잡아당기면 열리던 LAMY 스튜디오 만년필이랑은 다른 느낌이었다. 약간 당황하긴 했지만 돌려서 여니 14K 펠리칸 EF촉이 나왔다. 14K 도금이 되어 있고 M800의 18K에 비해서는 금함량이 적어 펜촉이 강성이라는 이야기를 카페에서 봤다. 어쨌든 투톤의 색깔을 가졌다. 슬릿 커팅상태를 보니 미묘하게 쏘쏘였다.

펠리칸 M600 만년필 펜촉

M600은 “피스톤필러”방식이기 때문에 잉크가 본체에 직접 충전된다.
충전 방법은 아래와 같다.
1) 펜 뒷 부분을 돌리면 피스톤이 아래로 내려오게 된다.
2) 잉크에 펜촉(펜촉의 구멍있는 부분까지 잠기게 해야함)을 담근다.
3) 펜 뒷 부분을 잠그면 주사기랑 동일한 원리로 잉크가 빨려들어가게 된다.
4) 끝까지 조이고 난 후, 살짝 풀어서 잉크를 3\~4방울 정도 떨어질 때까지 풀었다가 다시 꽉 조인다.
잉크는 가지고 있는 다른 만년필의 카트리지보다 더 많은 양의 잉크가 들어가는 듯 보였다. 잉크가 얼마나 남았는지는 투명창을 통해서 확인 가능하여 언제 잉크를 충전해야할지를 보여주고 있었다.

펠리칸 만년필 잉크 주입

잉크를 넣고 A4용지에 글을 써보았다. 역시 펠리칸 답게 물 흐르 듯이 부드럽게 글이 써졌다. 병목샷과 함께 사진을 찍어서 올려봤다. 색깔은 펠리칸 4001 다크 그린 잉크이다. 잉크는 글을 쓰고 바로 금방 마르는 듯 했다.

병목샷 & 필기샷

지금까지 펠리칸 M600 만년필을 리뷰해 보았다. 펠리칸의 긴 시간동안의 명성 답게 부드럽게 글씨가 써져서 기분이 좋았다. 크기는 나에게 딱 알맞아서 불편함은 없었다. 의외로 가벼운 무게에 놀랐다. 잉크는 많이 충전할 수 있어서 한 번 충전하면 카트리지 잉크보다는 더 오래 쓸 수 있을 듯 했다. 만년필에 관심 있으면 펠리칸 만년필에 도전해 보는 것도 좋을 것이라고 생각한다.

3월 192017
 

이번에 어떤 이슈를 확인해야할 일이 있어서 macOS의 콘솔 기능을 이용할 일이 있었다.
콘솔 기능은 앱이나 시스템으로부터 수집된 로그 데이터(통합 로그, unified logging)를 보기 쉽게 보여주는 프로그램이다.
하지만 분석을 하다보면 <private>로 되어 분석에 한계가 있는 로그들이 존재한다.

콘솔 기능에 들어가면 로 숨겨져 있는 것을 확인 가능

이는 macOS 내부에 제작된 프라이버시 프로텍션 기능이 동작해서 그렇다.
하지만 이를 풀어서 확인해야할 필요성이 있는 경우가 있는데 이 때 터미널을 켜서 아래의 명령어를 실행하면 된다.

sudo log config --mode "private_data:on"

그럼 아래 그림과 같이 <private>가 없어진 상태로 출력된다.

private로 숨겨지지 않은 형태로 출력

작업이 끝나고 다시 이 기능을 활성화 해야할 경우에는 반대로 실행하면 된다.

sudo log config --mode "private_data:off"

 

2월 162017
 

PS4 Pro가 나온지 벌써 3개월이 지났지만 여전히 재고는 없고 검색을 조금만 해보면 가격을 높혀 파는 매물만 존재하는 듯 하다. 이에 몇몇 사람들은 포기하고 PS4 Slim을 구매하는 사람들도 있고 어떤 분들은 해외에서 직구를 하거나 윗돈을 주고 중고나라에서 구매하는 사람들도 존재하는 것 같다. 본인 같은 경우는 A/S가 확실치 않은 직구를 하고 싶진 않고 중고를 그다지 좋아지지 않는 나는 이 길을 택하지도 않을 것이고 급하지도 않기 때문에 매물이 있는 PS4 슬림을 구매하지는 않는다. 그러면 한국에서 일반적인 경로로 물건을 구매하게 될텐데 크게 두 가지 경로가 있을 것이다. 하나는 오프라인을 통해서 구매하는 방법이 있고 다른 한 가지 방법이 온라인을 통한 방법이다. 오프라인의 경우 회사 휴가를 쓸 수도 없고 구매하는 곳까지 가야하고 줄도 서야하고 너무 귀찮기 때문에 온라인을 노려보기로 했다. 그런데 재고가 들어왔을 때 알려주는 기능이 있다면 상관 없지만 그게 아니기 때문에 귀찮은 것을 싫어하는 개발자 입장에서 홈페이지에 주기적으로 접속해서 재고 여부를 확인하는 프로그램을 만들고자 하였다.

요구사항

  • 지금 사용하고 있는 개인 서버(리눅스)에서 쉽게 설정해서 쓸 수 있을 것
  • 만드는데 10분안에 뚝딱 만들 것
  • 최대한 로직 간단하게 하기

그 결과 쉘 스크립트가 가장 만만한 선택지였고 홈페이지에서 웹페이지를 다운로드 받아서 문자열 분석해서 구현하는 것으로 하였다. 쉘 스크립트는 가끔씩 만들어 본 적이 있어서 익숙하다는 점도 한 목 했다.

알고리듬

1) PS4 구매 페이지를 다운받는다.
2) "재고없음"이란 글자가 있는지 확인한다.
3) 만일 "재고없음"이 존재하면
  3-1) n초 기다린다
  3-2) 1로 돌아감
4) 만일 "재고없음"이 없으면
  4-1) 비프음을 울리고 이메일 전송
  4-2) 종료

한계점 : 네트워크가 안된다던지 하는 경우는 제외, 구매 페이지 자체의 형식이 바뀌는 부분은 제외 등

구현 준비

웹 페이지의 구성 확인

간단한 구현을 위해서 웹페이지의 소스의 문자열을 파악하는 방식으로 구현하는 것을 고려하였다. 이에 따라서 재고가 있는 페이지와 재고가 없는 페이지를 비교했다. 어떤 차이점이 있는지를 알아야 문자열 비교를 할 수 있기 때문이다. 제일 좋은 점은 재고 있는 경우 또는 재고 없는 경우에 특정한 문자열(단어)이 존재하는 것이 최고이다. 운이 좋게도 “재고없음”이라는 단어가 재고가 있는 경우는 등장하지 않는 것을 확인 가능했다.

아래는 이를 위해 이용한 url이다. 물론 이건 2017년 2월 15일 현재에 확인한 결과이니 나중에 재고 없는데 왜 있냐라거나와 같은 이야기가 안나오기를 바란다.

크롬에서 공식PS4판매 페이지 소스코드 보기

리눅스 명령어 조사

리눅스 명령어를 조합해서 만드는 것이 매우 직관적이고 불필요한 재개발을 줄일 수 있는 좋은 선택이기 때문에 최대한 사용해 보겠다.

| – 파이프라인 (pipeline)

쉘에서 파이프(|)는 파이프 좌측 명령의 stdout을 파이프 우측 명령의 stdin으로 입력해주는 역할을 한다.

지금까지 조사했던 내용을 조합해 보면 wget -qO- https://store.sony.co.kr/handler/ViewProduct-Start?productId=50150445 | grep -c '재고없음'을 입력하면 재고가 없을 때 1이상의 값이 되며, 재고가 있을 때 0이 출력되게 되는 코드가 만들어 진다.

명령어 치환 (Command Substitution, “)

\`\을 이용하면 \`blah\라는 명령어를 실행하고 그에 따른 stdout텍스트로 치환하는 기능이다.
예를 들면 ls error.log.`date "+%Y%m%d"`라고 실행하면, ls error.log.20170216이 실제로 실행되게 된다.

wget – 웹페이지 다운로드

wget을 이용하면 웹 페이지의 내용을 가져올 수 있다. https나 http에 필요하면 로그인, 쿠키 등도 붙일 수 있고 옵션을 붙이면 stdout으로 결과를 뽑아볼 수 있기 때문에 다른 명령어와 연동하기에 편리하다.
여기서는 wget -qO- <<http location>>을 이용하여 웹 페이지(html)를 가져와서 stdout으로 출력한다.

grep – 문자열 검색

grep은 리눅스에서 잘 사용하면 많은 것을 할 수 있는 기능으로써 입력된 문자열에서 특정 문자열을 검색하는데 사용한다. 여기서 주목한건 -c옵션인데, 이를 이용하면 그 문자열이 입력된 문자열에서 몇 번 등장했는지 횟수를 stdout으로 출력할 수 있다.

예를 들어 echo "거북이 밥먹어 하하하" | grep -c '거북이'라는 코드를 입력하면 1을 출력하게 되는 것이다. 만약 echo "토끼 밥먹어 하하하" | grep -c '거북이'라는 코드를 입력하면 ‘거북이’라는 문자열이 없기 때문에 0이 리턴된다.

echo – 입력을 그대로 출력

셀 스크립트를 구현할 때, 현재 진행상황을 출력하기 위해서 사용되는 명령어이다. echo hello를 입력하면 stdout으로 hello가 출력된다.
-n 옵션 사용시 뒤에 개행을 하지 않는다. -e옵션 사용시 escape 문자열을 해석한다. ASCII에서 문자열 역할을 하지 않는 코드들을 생각하면 된다.
따라서 비프음을 쉘에서 울리고 싶다면, ASCII에서 비프를 의미하는 0x07 BEL을 이용한다. 응용해 보면 echo -ne '\007'을 입력하면 비프음이 울리게 된다.

sleep – 지정한 시간만큼 멈춤

sleep <<seconds>>명령어는 입력한 만큼의 숫자의 초만큼 프로그램을 정지시키는 명령어이다. 소수점도 이용 가능하다. 예를 들면, sleep 0.1이라고 입력하면 0.1초 동안 정지된다.

mail – 메일을 확인하거나 보내는 명령어

mail명령어를 전부 설명하면 범위를 벗어나기 때문에 보내는 방법에만 집중해보면, echo “PS4 Pro 재고가 있어요!!!” | mail -s “[알림]PS4Pro 재고 있음!!” blah@gmail.com과 같이 입력하면 제목을 [알림]PS4Pro 재고 있음!!으로, 내용은 PS4 Pro 재고가 있어요!!!, 보내는 주소는 blah@gmail.com이 되는 것이다. 혹시 전송이 안되거나 스팸메일함으로 전달 된다면, 믿을 수 있는 smtp 서버를 지정하면 되는데, 이 부분은 범위를 벗어나니 linux sendmail이란 키워드로 검색해 보는 것을 추천한다.

exit – 종료

쉘에서 나갈 때는 exit를 사용하면 종료되게 된다.

쉘 스크립트 언어

지금까지 위의 명령어를 이용하면 위의 알고리듬을 실행하기 위한 구성요소들은 거의 다 준비되었다고 볼 수 있다. 하지만 여기서 문제는 재고없으면 어떻게 다시 실행시킬 것인지 재고가 있는 경우 어떻게 알릴 것인지가 해결이 되지 않았다.
이 부분을 쉘 스크립트를 통해서 해결한다. 여기서는 많이 사용되고 있는 bash쉘을 이용하여 구현해 보겠다.

시작 부분

쉘 스크립트의 시작부분에는 #!/bin/bash를 입력한다. 그러면 이 쉘 스크립트의 내용을 실행할 때 /bin/bash를 통하여 동작시키게 된다. bash를 사용하지 않았으면 해당하는 언어를 적으면 된다. 파이썬이나 펄도 마찬가지 방법으로 이용할 수 있다.

변수대입

변수 대입은 변수명=대입할내용를 통해서 할 수 있다. 숫자 10abc란 변수에 넣고 싶으면 abc=10이라고 입력하면 된다.

변수사용

변수를 사용할 때는 변수명 앞에 $기호를 붙인다. 예를 들어서 abc란 변수를 사용할 떄는 $abc로 입력한다.

조건문

아래와 같은 형식으로 조건문을 사용할 수 있다.

if [<<조건>>]; then
   <<내용>>
fi

반복문

while 인 경우

while <<조건>>
do
   <<내용>>
done

for를 쓸 경우 (1~30까지 1씩 증가하면서 변수 i에 넣음)

for i in {1..30}
do
   <<내용>>
done

숫자계산

쉘 스크립트에서 숫자에 대한 사칙연산 등은 아래의 명령어를 사용한다.
$((<<expr>>))

예)
a=$((1+2+3)) => $a6입력
b=$((a+10)) => $a6이므로 $b16이 입력
c=$((b-1)) => $a6, $b16이므로 $c에는 10이 입력

주석처리

# 등장 후 개행까지는 전부 주석으로 취급함

실제 코드 구현

위에서 간단하게 배워본 쉘 스크립트를 통하여 실제 프로그램을 구현해 보았다.
알고리듬을 기준으로 구현한 코드는 아래와 같다.

#!/bin/bash

retry_count=0

while true
do
        # 50150247 : PS4 slim
        # 50150445 : PS4 Pro

        echo "try #$retry_count... "
        retry_count=$((retry_count + 1))

        result=`wget -qO- https://store.sony.co.kr/handler/ViewProduct-Start?productId=50150445 | grep -c '재고없음'`

        if [ $result == 0 ] ; then
                echo '재고 풀렸음!!!'

                # beep 음
                for i in {1..30}
                do
                        echo -ne '\007'
                        # 30fps
                        sleep 0.03
                done

                # 메일 전송
                echo “PS4 Pro 재고가 있어요!!!” | mail -s “[알림]PS4Pro 재고 있음!!” blah@gmail.com

                # 종료
                exit
        fi

        # retry each 60 seconds
        sleep 60
done

vinano와 같은 텍스트 에디터를 열어서 ps4_checker.sh이라는 이름으로 만든다.
그 이후 쉘이 실행될 수 있도록 chmod u+x ps4_checker.sh을 실행하여 실행권한을 부여한다.

비프음의 경우 sleep으로 끊어서 처리하지 않으면 중첩되서 한 번 밖에 들리지 않게 된다.

실행방법

./ps4_checker.sh 을 실행한다. 그러면 60초마다 한 번 씩 재고유무를 체크하게 된다.
실제로 재고 있을 때 동작하는지 ‘PS4 Slim’웹페이지 주소를 이용하여 테스트 해보도록 하자.

재고 있는 경우

$ ./ps4_checker.sh
try #0...
재고 풀렸음!!!
$

성공적으로 이메일이 오는 것을 확인할 수 있다. 만약 안보이면 스팸메일함을 확인해 보자. 그래도 없으면 mail관련 리눅스 설정이 추가적으로 필요할 수도 있다.

재고가 있다는 이메일을 받은 화면

재고 없는 경우

$ ./ps4_checker.sh
try #0...
try #1...
try #2...
try #3...
try #4...
try #5...

강제 종료는 “Ctrl+C”를 하면 된다.

한계

“재고없음”이라는 글자가 없는 것을 판단하기 때문에, 인터넷이 끊기게 되면 재고있다고 오작동 하게 된다. 이 부분은 여러분이 수정해 보도록 하자! 🙂

결론

이 글은 어떻게 이 프로그램을 만들게 되었고 어떤 요구사항을 따르게 해야하는지 정리하였으며 구현 시작하기에 앞서 실제 PS4판매 페이지의 특성을 분석해보았으며 이에 알맞는 프로그램을 개발하기 위해서 쉘 스크립트 언어와 여러 리눅스 명령어를 조사하여 실제로 구현하여 잘 동작하는 것을 확인할 수 있었다. 물론 인터넷이 끊기거나 홈페이지가 변경되었을 때에 대응책에 대한 부분은 최대한 간단히 만들기 위해서 고려상황에서 제외하였다. 또한 대몬(daemon)화 시켜서 동작시키는 부분도 범위 밖이기 때문에 다루지 않았다. 이 글을 통해 꼭 C나 자바와 같은 프로그래밍 언어를 공부해야만 프로그램을 만들 수 있는게 아니라 리눅스에서 제공하는 유용한 명령어들을 조합하여 구현하는 것으로도 충분히 쓸만한 유용한 프로그램을 만들 수 있다는 것을 알 수 있었다. 이를 응용하면 꼭 이 상황뿐만 아니라 다른 홈페이지를 감시하는 경우에도 활용할 수 있을 것이다.

2월 022017
 

이 글은 Qitta의 geshi님이 작성한 テストがうまくいかないプロジェクトに捧ぐ、正しいテストの考え方, 2016년 3월 9일 갱신 글을 번역한 것입니다.

이 글의 대상자

  • 프로젝트에서 테스트를 작성하고 있다. (작성한 적이 있음)
  • 테스트가 중요하다는 것은 알고 있지만 테스트의 덕을 제대로 실감한 적이 없다.
  • 결국 수동 테스트에 의존해 버그를 고치고 있다.

시작하면서

저는 테스트의 설계방법과 구현에 관한 지식은 많이 지니고 있었지만 잘 몰랐던 것은 테스트에 대한 사고방식이였습니다.

테스트의 중요성에 대해서 알고 있는 사람은 많이 있을 것이라 생각합니다. 하지만 실제로 테스트의 덕을 보지 못하고 있는 사람도 있을 것이라 생각합니다.
사실, 테스트가 중요하다고 말로만 하는 사람테스트의 중요성을 알고 있는 사람 이 있습니다. 후자의 경우, 일단은 테스트를 작성하는 것이 가능합니다. 하지만, 테스트에 시간을 할애하면 할애할 수록 최종적으로는 수동 테스트에서 버그를 발견하는 것에 의존하고 있는 경우가 많을 것이라 생각합니다.

세간에서는 “테스트를 작성하는 것이 당연하고 테스트는 중요해!”라는 풍조에 있는데 왜 테스트가 중요한 것이라고 실감은 할 수 없는 것일까요?? 어떻게 해야만 테스트가 유효하게 활용되고 있는 프로젝트가 될 수 있는 것일까요?

이번의 기사에서는 그러한 문제에 대하여 잔재주가 아니라 실전에서 “어떻게 사고하는가?”라는 사고회로를 설명하고 싶다고 생각합니다.

예를 들면, 이하와 같은 케이스를 생각해 보겠습니다.

케이스 스터디

class SampleController
  def index
    # do something
  end
end

이 액션 안에서는 A,B,C,D,E,F,G,H,I,J라는 10개의 메소드가 호출되어, 각각(0, 1)을 리턴하고 1024가지의 행동거지가 존재합니다.

여기서, 아래의 상태일 경우에는 정상인 행동거지를 하지 못한다는 레포트가 올라왔습니다.

레포트 1

A -> 0
B -> 1
C -> null
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> - 2

레포트 2

A -> 0
B -> 1
C -> 0
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> 1

버그가 재발되지 않도록 하기 위해서는 어떤 테스트를 해야만 하는 걸까요??

여기서, 유닛테스트만을 작성하는 프로젝트에서는 레포트1의 버그를 막는 것은 가능해도 레포트 2의 버그를 막는 것은 어려운 경우가 있겠지요.
이 케이스를 기본으로 하여 “어떤 테스트 케이스를 작성해야만 할까요?”라는 부분에 대해 설명해 보도록 하겠습니다.

전제

갑작스럽게 방법론으로 들어가는 것이 아니라 “애초에 테스트의 목적은 무엇인가?”, “이상적인 테스트란 무엇인가?”, “이상적인 테스트의 문제점은 무엇인가?”라는 점에 대해 해석을 통일하고 싶습니다.

  • 테스트의 목적
  • 버그를 막는데 어떻게 하는게 적절할까?
  • 이상적인 테스트란?
  • 이상적인 테스트의 문제점

테스트의 목적

테스트의 목적은

  1. 버그를 발견하는 것
  2. 품질을 보증하는 것
  3. 품질을 개선하는 것

이라고 알려져 있습니다. 이 중에 가장 중요한 목적은 1. 버그를 발견 하는 것입니다.
즉, 극단적으로 말하면 버그가 없는 어플리케이션이 이상적인 것이며, 그 이상의 근쳐로 다가가기 위해 좋은 수단 중의 하나가 테스트라는 것입니다.

버그를 막기 위해서는 어떻게 해야하나?

사양(최종 구조/디자인), 프로그램 설계, 테스트라고 하는 세가지의 영역으로 나눠져 있다고 생각합니다.
사양, 프로그램 설계가 나쁘다면 테스트도 힘들어질 것입니다.
그렇기 때문에 스파게티코드는 리팩토링하고 나서 테스트해 주십시오.
하지만, 이번은 테스트에 대한 이야기 입니다.

이상적인 테스트란?

그러면, 이상적인 테스트란 어떤 테스트인 걸까요?
그것은 모든 유스케이스가 정상적으로 동작하는 것 을 증명하는 테스트입니다.

즉 100가지의 사용방법이 있다면 100가지의 응답이 올바른지 어떤지 테스트한다면 완벽하게 버그를 발견할 수 있습니다.
앞서의 케이스를 이용해 설명해보겠습니다.

케이스 스터디

class SampleController
  def index
    # do something
  end
end

이 액션 중에는 A,B,C,D,E,F,G,H,I,J라고 하는 10개의 메소드가 호출 되어 1024가지의 유스케이스가 존재합니다.
또한, 이런저런 메소드는 모두 (0, 1)을 리턴합니다.

여기서 아래의 상태일 때 버그가 발생한다는 레포트가 올라왔습니다.

A -> 0
B -> 1
C -> 0
D -> 1
E -> 1
F -> 1
G -> 0
H -> 0
I -> 0
J -> 1

회답 (이상적인 경우)

일단, 조합에 따라서 버그가 발생하고 있는 것인가, 개별 메소드가 버그를 발생시키고 있는 것인가 알지 못합니다.
하지만 이번에는 그렇게 된 상황을 무시하고 이상적인 입장에서 테스트 케이스를 생각해 보겠습니다.

이상적인 입장에서는 모든 조합이 정상적인지 테스트합니다.
즉, 2 ^ 10 = 1024가지의 테스트를 한다면 버그가 완벽하게 발견되는 것이 가능할 것입니다.

이상적인 경우의 문제점

유스케이스가 1024가지가 있는 기능이라는 것은 무슨 괴물인가 싶습니다만, 현실적으로는 보통의 어플리케이션에 하나의 액션(기능)에 대하여 100가지의 테스트를 하는 것은 불가능합니다. 또한 복수의 액션(기능)이 결합해간다면 무한의 테스트 케이스가 필요하게 됩니다. 여기서 문제점은 모든 유스케이스를 테스트하는 것은 공수적으로 불가능하다는 것입니다.(당연한 말입니다만..)

현실적인 테스트 설계 방법

이상적인 테스트를 목표로 하면 할 수록 지수함수적으로 공수가 늘어납니다.
그렇기 때문에 소프트웨어 테스트의 세계에서는
얼마나 비용대비이득이 높은 테스트를 작성하는 것이 가능한가?
라는 것이 명제가 되어 왔습니다.

그렇다면 비용대비이득이 높은 테스트는 어떻게하면 작성할 수 있을까요?
그것은 버그가 발생하기 쉬운 코드에 대하여 제대로 테스트 코드를 작성하는 것 입니다.
거꾸로 말하면, 이상적인 테스트로부터 중요하지 않은 조건을 솎아내는 것입니다.
그렇다면 지금부터 어떻게 이상적인 테스트케이스로부터 현실적인 레벨로 솎아내어 가는지 세 가지 방법을 설명하겠습니다.

  • 방법1 : 독립된 요소를 솎아낸다.
  • 방법2 : 중요도가 낮은 패턴은 테스트를 하지 않는다.
  • 방법3 : 통계정보에 기반으로한 조합으로 좁힌다.
  • 요약

방법1 : 독립된 요소를 솎아낸다.

이 방법은 테스트를 현실적으로 하는 가장 베이스가 되는 사고방법입니다.

케이스 스터디

class SampleController
  def index
    # do something
  end
end

이 액션 중에는 A,B,C,D,E,F,G,H,I,J라는 10가지의 메소드가 호출되어 1024가지의 유스케이스가 존재합니다.
또한, 각각의 메소드는 모두 (0, 1)을 리턴합니다.

명제1 : 「1024가지 유스케이스는 전부 정상」 이라면 「10개의 메소드는 전부 정상」
명제2 : 「10가지 메소드는 모두 정상」 이라면 「1024가지의 유스케이스는 모두 정상」

명제1과 명제2는 각각 참과 거짓 중 어느 쪽일 일까요?

회답

명제1은 참, 명제2는 거짓입니다. (함수형언어를 사용하면 후자라도 참이 되는 프로그램을 작성할 수 있는 듯 합니다만..)

전자는 210 = 1024가지의 테스트를 할 필요가 있으며,
후자는 2*10 = 20가지의 테스트가 조건입니다.

위의 명제는, 유닛테스트만으로 버그를 발견하기 위해서는 불충분하다 는 것을 증명합니다. 하지만 실제로는 솎아내는 것 만으로도 충분한 경우가 있습니다.

예를 들면, 10개의 메소드 안에 D와 H 메소드가 완전히 독립하여 있는(다른 메소드에 대해 영향을 주지 않음) 경우, D와 H는 0 또는 1을 리턴하는 것이 증명이 되어있는 것 만으로 문제없습니다.
또한, E와 F도 각각 영향을 끼치는 스코프가 좁아 조합 테스트에 필요가 없다고 판단할 수 있습니다.

즉, 유스케이스의 지수는 10에서 6으로 줄어들어 유닛테스트가 4개 증가합니다.
2 ^ 6 + 2 * 4 = 70가지

이렇게 해서 독립해 있는 조합을 솎아내 갑니다.

포인트

독립해 있는 메소드는 유닛테스트로 충분하여 조합의 지수가 줄어든다.

방법2 : 중요도가 낮은 패턴은 테스트하지 않는다.

이것은 이론적으로 결정할 수 있는 것은 아닙니다. 케이스 바이 케이스입니다만 사고방법을 알고 있는 것으로 오차는 허용되는 범위로 들어가겠죠.

케이스 스터디

class SampleController
  def index
    # do something
  end
end

이 액션 내에는 A,B,C,D,E,F,G,H,I,J라는 10개의 메소드가 호출되어 1024가지의 유스케이스가 존재합니다.

또한, A라는 메소드는 라이브러리에서 제공되어있는 인증기능입니다. 인증된 경우에는 1, 인증되지 않는 경우는 0을 리턴합니다.
また、Aというメソッドはライブラリーで提供されている認証機能です。認証の場合1 認証でない場合0を返します。
F라는 메소드는 ‘red’, ‘green’이라는 값을 리턴하여 버튼의 색을 바꿉니다.
H라는 메소드는 사용자가 입력했던 정보로부터 플랜A오브젝트, 플랜B오브젝트를 리턴합니다.

이 중에는 테스트하는 것이 좋은 케이스와 테스트하지 않아도 되는 케이스라고 하는 서열이 존재할 터입니다. 그렇다면 어떻게 테스트의 우선순위를 결정해야하는 것일까요??

회답

(중요도) × (버그유입리스크) 라는 매트릭스로 판단합니다.

제3회:테스트항목의 범위 축소:무게를 제는 것은 필히 수치로 표현하자
라고 하는 기사에서 소프트웨어의 중진분들이 의견을 기술하고 있습니다.

중요도라는 것은 사용자적으로 중요, 영향범위가 큰 것 등의 요소가 있습니다.
버그유입리스크란 사양적으로 소스코드적으로 복잡한 곳할 수록 커집니다.(결합도가 높은 곳)
버그유입리스크는 매트릭스해석을 하여 어느정도는 정량적으로 판단하는 것이 가능하기도 합니다. (매트릭스 해석에 대해서는 추후에..)

즉, 이번의 케이스라면 A는 신용할 수 있는 라이브러리로 제공되기 때문에, 버그가 발생할 리스크는 적다.
F영향범위가 좁아지고 사용자적으로도 그렇게까지 중요하진 않다.
H는 어플리케이션의 가치 그 자체로써 로직도 복잡하다.

이러한 매트릭스가 되어 A는 유닛테스트를 할 필요가 없고 F는 조합 패턴에 넣을 필요가 없다고 판단할 수 있습니다.

포인트

(중요도) × (버그유입리스크)를 고려해 유닛테스트가 필요없는 코드, 조합의 패턴에 넣을 필요가 없는 개소를 솎아낸다.

방법3 : 통계정보에 기반한 조합으로 좁힌다.

여기까지 “어떤 테스트케이스를 솎아내는 것이 좋은가?”라고 하는 베이스가 되는 사고방식을 알게 되었습니다.
하지만 인간의 힘으로는 하나의 기능에 대해서 수십개의 테스트케이스조차 빡샌 곳도 있습니다.
여기서 테스트엔지니어 분들은, “어떤 때에 버그가 발생하는가?”라는 것을 통계정보라고 하였습니다.
그러자 “2파라메터 사이까지로 발생한 결함이 7~9할을 차지한다.”” 라고 하는 통계결과가 발견되었습니다. 이 데이터를 기초로 해서 고려된 테스트 설계방법이 페어와이즈법(pairwise) 라고 불리우는 방법입니다.

케이스 스터디

class SampleController
  def index
    # do something
  end
end

이 액션 중에는 A,B,C,D라는 3가지의 메소드가 호출되어 8가지의 유스케이스가 존재합니다.

페어와이즈법이란 아래와 같습니다.

조합테스트기법의 한가지인 올페어(페어와이즈)법에서는, 모든 파라메터에 대하여 적어도 2파라미터사이에서의 값의 조합이 망라되어있듯이 테스트패턴을 작성합니다.

올페어(페어와이즈)법 (일본어)

라고 적혀있습니다.

즉, 2개의 메소드의 조합이 모두 망라되어있다면 문제 없습니다.
예를 들면 A,B,C,D라고 하는 메소드를 페어와이즈법으로 테스트케이스를 생각해보면 아래와 같이 됩니다.

A       B       C       D
1       0       0       0
1       1       1       1
0       1       0       1
0       0       1       1
0       1       1       0

페어와이즈를 작성하는 툴이 존재하는 듯 하여 이를 사용해 보는 것도 좋을지도 모르겠습니다.

조합테스트케이스 생성툴 PictMaster와 소프트웨어 테스트의 화제(일본어)

요약

방법1, 독립해있는 패턴을 찾아봅시다.
방법2, 정말로 테스트해야만 하는지 매트릭스로 생각해봅시다.
방법3, 현명하게 통계정보에 기반으로해 조합의 수를 줄여봅시다.

그렇게 한다면 현실적인 조합이 될 거라고 생각합니다.

위의 실천으로 테스트를 작성할 때에 알아둔다면 좋은 지식

이상으로 베이스가 되는 사고방식을 몸에 익히는 것이 가능하였습니다.
하지만 지금까지의 케이스는 어느정도 이상화된 모델을 베이스로 했던 케이스 스터디입니다.
지금까지의 케이스로는 조합이 매우 명확했습니다.
하지만 현장의 코드에서는 조합의 수는 사람에 따라서 다르다는 것이 발견되기도 합니다.
또한 사람에 따라서 “어떤 코드에 버그가 숨어 있는가?”라는 해석에도 이견이 생겨날 것입니다.

그렇다해도 프로젝트에서 개발할 때는 가능한한 공통의 인식을 지닐 것입니다.
거기서 이하의 항목에 대한 지식의 설명을 하고 싶습니다.(알고있는 항목이라면 넘어가주세요.)

  • 커버리지율
  • 동치분할과 경계값분석
  • 매트릭스 해석

커버리지율이란 경로의 조합을 세는 방법입니다.
동치분할과 경계값분석이란 값의 패턴을 추출하는 방법입니다.
매트릭스 해석이란 이런저런 코드에 버그가 숨어있는지를 정량적으로 측정하는 방법입니다.

이것을 기억하는 것으로 어느정도 프로젝트간에 조합선택의 불균형이나 우선순위의 불규칙성이 없어질 것입니다.

커버리지율이란?

프로그램 내의 분기를 어느정도 망라했나를 나타내는 방법입니다.
커브리지율에는 C0, C1, C2라는 레벨이 있습니다.

이 코드의 예로 설명하겠습니다.

def my_method
  if type1 == "A"
    print("처리1")
  else
    print("처리2")
  end

  if type == "B"
    print("처리3")
  end
end

C0 : 명령망라

C0레벨에서는 명령을 망라해있는지 어떤지를 판단합니다.
즉 처리1 ~ 처리3을 한 번씩 지나가면 문제 없습니다.

def my_method
  if type1 == "A"
    print("처리1")
  else
    print("처리2")
  end

  if type == "B"
    print("처리3")
  end
end

테스트 케이스는 이하의 두 개로 C0레벨의 커버리지율이 100%라고 할 수 있습니다.

type == "A" TRUE,  type == "B" TRUE
type == "A" FALSE, type == "B" TRUE

C1 : 분기망라

C1레벨에서는 분기를 망라해있는지 어떤지를 판단합니다.
즉 모든 분기를 한번씩 통과하면 오케이입니다.

def my_method
  if type1 == "A"
    print("처리1")
  else
    print("처리2")
  end

  if type == "B"
    print("처리3")
  end
end

테스트 케이스는 이하의 4가지로 C1레벨의 커버리지율이 100%라고 말할 수 있습니다.
2 + 2 = 4가지

type == "A" TRUE,  type == "B" TRUE
type == "A" FALSE, type == "B" TRUE
type == "A" TRUE,  type == "B" FALSE
type == "A" FALSE, type == "B" FALSE

C2: 조건망라

C2레벨에서는 분기의 조합도 전부 망라했는지 아닌지 판단합니다.
이것은 이상적인 테스트에 꽤 가까워서 큰일입니다.

def my_method
  if type1 == "A"
    print("처리1")
  else
    print("처리2")
  end

  if type == "B"
    print("처리3")
  end
end

테스트 케이스는 이하의 4가지입니다만, 실제로는 조건이 늘어나면 지수함수적으로 테스트케이스가 늘어 납니다.
2 * 2 = 4가지

type == "A" TRUE  && type == "B" TRUE
type == "A" FALSE && type == "B" TRUE
type == "A" TRUE  && type == "B" FALSE
type == "A" FALSE && type == "B" FALSE

동치분할과 경계값분석

그럼 여기까지 설명으로 조건분기의 수로 조합이 카운트되는 것이 이해되었습니다.
하지만, 아직 하나 더 조합의 변수가 있습니다. 그것은 값을 취할 수 있는 범위입니다.(치역)

케이스 스터디

이하의 코드의 테스트전략에 대하여 생각해보겠습니다.


# var이 꺼내오는 값은 String, Int, nil의 어느것인가?
def my_method(var)
  var.your_method
end

커버리지적으로는 한 번으로 충분합니다
하지만, 커버리지율만 생각하는것으론 불충분합니다.
이번이라면 String, Int, nil의 케이스를 최저한으로 테스트하고 싶을 것입니다.

커버리지가 경로의 망라라면 이번에는 값의 망라입니다.
하지만, 값의 망라는 무한의 가지수가 있습니다 (자연수만 해도..)
그러면, 어떻게 망라하면 좋을까요??

동치분할

예와 같이 해설은 다른 이해하기 쉬운 기사에 맏기겠습니다.

동치분할이란 뭘까?(일본어)

경계값분할

그 이름 그대로 경계값을 테스트하는 것입니다.

매트릭스 해석이란

소프트웨어를 정적으로 분석하여 여러가지 관점으로부터 품질을 정량평가하는 것입니다.

이 사고방식을 아는 것으로 어떤 소스코드에 버그가 많은가? 라고 하는 것을 어느 정도 알 수 있습니다. 자세한 설명은 이하에 참고링크를 적어두겠습니다만 순환복잡도라고 불리우는 지표만 설명하겠습니다.

순환복잡도란?

소스코드 경로의 수를 세는 것으로 복잡도를 표현하는 방법입니다.
그렇다고 하더라도, 여기의 기사가 압도적으로 알기 쉽게 되있으므로 꼭 봐주세요.

\버그다-!/ 시스템에 서식하는 버그님으로부터 본 퀘적함, 순환복잡도(Cyclomatic Complexity)란? \버그다-!/ (일본어)

참고링크

매트릭스 해석이란?
Ruby on Rails | metric_fu로 매트릭스 해석 (일본어)
정적코드해석툴인 MetricFu는 무엇을 보고 있는 것인가? (일본어)

각언어 별 매트릭스해석 툴
처음하는 소프트웨어 매트릭스(전편) : 소프트웨어의 품질을 수치화하여 확인한다 (일본어)

테스트 케이스를 선택하는 방법을 고르고 싶을 때 읽는다.

「지식제로로부터 배우는 소프트웨어 아티스트」를 읽고 「화이트박스 테스트부터 탐색적 테스트까지」 – (일본어)

Q&A

거의 다 작성했습니다. 하지만, 프로젝트멤버 전원이 테스트의 품질을 지키는 것은 어렵습니다.
여기서, 지금까지 나온 논점 등을 Q&A방식으로 작성해보도록 하겠습니다.

질문1 : 유닛테스트와 E2E테스트는 어떻게 가려 써야 하나요?

이상적으로는 모두 함수, 메소드의 행동거지에 대하여 유닛테스트하여
그것들의 모든 조합을 E2E테스트로 테스트하는 것입니다.
하지만 현실적으로 그것은 어렵습니다.
그래서 복잡도가 낮은 함수, 메소드는 유닛테스트를 하지 않습니다.
독립해있는 조합은 테스트 케이스로부터 솎아냅니다.
이런 사고방식으로 테스트를 설계합니다.

번역 후기

이번이 세 번째 번역인데 꽤 장문의 글이였습니다. 후반부는 다른글로 링크가 많아서 자세히 이해하기 위해서 부족한 부분도 있겠지만 앞 부분에서 테스트를 어떤 생각을 해서 작성해내는지에 대한 부분만으로도 꽤 좋은 내용이 많기 때문에 문제가 없을 것이라 생각합니다. 같은 한자 문화권이고 일본의 영향을 많이 받아 해석할 때 기계적으로 하게 될 줄 알았는데 의외로 한자 단어들이 한국이랑 다르게 쓰이는 경우가 있었어서 재미있게 번역한 글인 것 같습니다.

1월 232017
 

이번 출장에서 만년필 써보기라는 소소한 도전을 위해 이번에 해외 출장 가면서 사왔다.  만년필의 부드럽게 물 흘러가듯 써지는 필감이 궁금하기도 했고 다른사람과는 다른 무언가를 하고 싶었기 때문이다. 만일 국내 오프라인이나 온라인에서 구매하면 각인서비스를 받을 수 있다고 하였지만 그다지 끌리지는 않았다. 그래서 이번 출장에 면세점에서 구매하게 된 것이다. 어떤 모델로 할까 고민하다가 라미사에서 만든 스튜디오 만년필을 선택하게 되었다. 10만원을 근방에서 고급스러워 보이는 디자인을 가졌기 때문에 선택하였다. 잉크가 들어있는 만년필을 가지고 비행기를 타면 비행중에 잉크가 기압차 때문에 새어나올 수도 있다고 해서 집에 돌아와서야 드디어 써볼 수 있었다. 첫 경험을 잉크폭발로 시작하고 싶은 건 더더욱 아니니까 말이다. 닙(펜촉) 크기에 따라서 글씨의 두께가 달라진다. UEF < EF < F < M < B < BB 와 같은 형식으로 두께가 달라지는데 일반적으로 한국어는 영어보다 좁은 공간에 많은 내용이 들어가기 때문에 EF를 많이 선택한다고 한다. 닙 부분을 자세히 보면 EF라고 써있는 것을 확인 가능하다.

케이스는 플라스틱 재질은 아니었고 종이계열의 재질이었다. 안에 어떻게 생겼는지 확인 할 수 있도록 투명하게 되어 있었다. 안에 진하고 푸르스름한 광택을 보이는 만년필이 보였다. 파랑색이 진하고 빛에 비추면 색깔이 푸르게 빛나는 것이 잘 구매했다는 생각을 들게 했다.

케이스는 자석으로 붙는 형태로 되어 있었으며 열어보면 위와 같이 만년필, 컨버터, 카트리지가 들어있다. 실제로는 파란색 카트리지가 들어 있었지만 이미 세팅을 한 관계로 검정색 카트리지를 대신 넣어서 찍었다. 컨버터는 잉크병에서 잉크를 뽑아서 사용할 수 있어 잉크만 계속 제공된다면 계속 쓸 수 있다는 장점이 있다. 하지만 카트리지 보다는 잉크 충전량이 적다. 카트리지의 경우 쓰기에는 편리하지만 다 사용하면 카트리지를 계속 교체해줘야 한다는 점이 있는데 어딘가에서 이야기 하기를 많이 쓴다면 잉크병을 구매해서 쓰는것이 카트리지보다 비용이 저렴하다는 장점이 있다고 한다.

카트리지와 컨버터 크기 및 용량 비교

 

컨버터를 만년필에 설치한 상태에서 닙부분을 잉크에 넣고 빨간 부분을 돌리면 잉크가 뽑아져 나오는 형식으로 동작한다.

만년필에 카트리지를 설치하고 사용하면 잉크가 나오기 시작한다. 잘 안나오면 안쓰는 팬을 안쓰는 종이를 놓고 그 부분으로 잉크가 나오도록 털어주면 만년필에서 잉크가 나오기 시작한다. 필기 할 때 55도 각도로 닙과 종이와 만나는 부분이 일직선이 되도록 하여 쓴다. 쓸 때 힘을 크게 주지 않아도 자연스럽게 글이 써지니 쌔게 누르거나 하는 것은 금물이다. 닙이 손상될 수도 있다. 글씨를 잘 썼으면 좋겠지만 아래와 같은 형태로 글이 써진다.

카트리지의 경우 라미 만년필은 거의 다 호환 된다고 하여 검정색으로 구매를 하였다. 오프라인매장에서 약 4000원 정도에 구매 하였는데 하나를 사면 5개가 들어 있다. 다양한 색깔이 있는 것을 볼 수 있었다. 검정, 파랑은 물론이고 빨강, 주황 등등 여러가지가 있었다.

만년필은 계속해서 매일 사용하지 않으면 잉크가 굳어버릴 수도 있으며 주기적으로 청소를 해주어야만 반영구적으로 사용할 수 있다고 한다. 나는 매일 컴퓨터만 치면서 글씨를 거의 쓸일 없는 사람이지만 이번 기회로 무언가를 노트에 적어가면서 생각하는 것을 다시 도전하게 되었다. 타자로 글자를 두두리려서 쓰는 것과 글씨로 쓰는 것은 많이 다르니까 말이다.

 

1월 192017
 

64비트 Objective-C에 대한 글을 쓰기 앞서서 C 언어 데이터 모델이 무엇인지 이야기하고자 한다. 64비트 시스템이 나오면서 기존의 플랫폼에 따른 타입들의 크기를 재정의 해야할 필요가 생겼는데 이에 대한 이야기이다. 플랫폼에 따라 변하는 기본 타입들은  int, void*(pointer), long, long long 등을 말한다. 물론 16비트 CPU에서 32비트로 전환되던 시절에도 이 이슈는 존재하였다. 예를 들면 16비트 cpu의 경우 16비트의 주소값으로는 65.536Byte 밖에 표현할 수 없는데, 이에 20비트의 포인터를 사용하였었다(모든 기종이 그렇다는 것은 아니다). 여기서 문제는 처리 단위가 16비트이다 보니 segment value(기준 주소를 4비트 시프트)와 offset(segment value로 부터의 실제 좌표의 차이)과 같이 포인터를 좀 더 복잡하게 고민하여 처리해야했다. near pointer와 far pointer로 구분하여 사용하던 시절이 있었다. 어쨋든 32비트에서 64비트로 바뀔 때도 비슷한 이슈가 있었는데, 이 것이 지금 말하려는 C언어 데이터 모델이다.

컴파일러나 플랫폼에 따라 선택한 데이터 모델은 달라진다. 기존 32bit 프로그래밍 모델에서는 ILP32를 사용한다. LP32는 기존 Win16CAPI에서 사용되곤 했다. LLP64의 경우 윈도우에서 채택[2] 했으며, LP64의 경우 일부 유닉스 계열에서 사용한다.

여기서 L은 long에 해당하며, P는 pointer size를 이야기 하며, I는 int를 의미, LL은 long long을 의미한다. long long의 경우 지원하지 컴파일러에 따라 지원하지 않을 수도 있다[2].

Datatype LP64 ILP64 LLP64 ILP32 LP32
char 8 8 8 8 8
short 16 16 16 16 16
int 32 64 32 32 16
long 64 64 32 32 32
long long 64
pointer 64 64 64 32 32

위의 테이블을 보면 각 데이터 모델에 따라서 어떤 값을 가지게 되는지 알 수 있다. pointer의 경우 확실히 다르기 때문에 기존 32bit 코드를 64bit로 변경할 때 꼭 주의 하여야 한다. long type의 변수에 포인터 값을 할당하여 사용하는 경우도 멀티플랫폼을 지원하기위한 개발을 한다거나 할 때 위험하기 때문에 잘 고려해서 선택해야한다.

차라리 C99에서 정확한 사이즈가 지정된 데이터 타입이 추가되었는데, 컴파일러가 C99와 호환된다면 이를 선택하는 것이 좋을 수도 있다. int8_t, uint8_t, int32_t, int64_t 등과 같이 unsigned된 형태와 8,16,32,64를 지원한다. 하지만 문제 점도 있는데, int32_t를 썼는데 16비트 컴퓨터에서 동작시킨다면 성능이 나빠질 수도 있다던지 하기 때문에 상황에 알맞게 구현해야할 것이다. 32비트와 64비트 빌드를 둘 다 지원하는 경우도 마찬가지로 데이터 타입의 사이즈를 고려해야 한다. 또한 size_t인 데이터도 사용할 때 사이즈에 주의해야한다.

참고자료

[1] Unix.org, “64-Bit Programming Models: Why LP64?”,  http://www.unix.org/version2/whatsnew/lp64_wp.html

[2] Hook, B. (2005). Write Portable Code: An Introduction to Developing Software for Multiple Platforms. No Starch Press., pp85-89

1월 132017
 

이 글은 qiita에 작성된 http://qiita.com/motokiee/items/b30514204a819a09425b(작성자 motokiee님, 2016-02-29 투고)을 번역한 글입니다.

Objective-C에서 Swift로 이전하는 과도기

수년간 개발되어온 앱에 슬슬 Swift를 도입하기 시작하지 않았나요? 당연히 Objective-C에서 만들어진 재산을 그대로 둔 상태로 개발하게 될 것이라고 생각합니다.

이 때 Optional을 다루는 것에 대해 곤란해하고있지 않을까 생각이 듭니다. Objective-C에서는 리시버가 nil인 상태로 메시지를 보내더라도 크래시가 발하지 않았지만, Optional이 있는 Swift로 부터 Objective-C의 코드를 호출하는 경우에는 좀 곤란해 집니다.

Swift와 Objective-C의 호환성을 강화하기 위해서 nullable, nonnull이 Objective-C에 추가되었습니다.

Swift의 코드를 작성할 때, 이러한 형수식자(Type qualifier)를 사용해서 Objective-C 쪽도 개선하여 Swift 도입을 좀 더 쉽게 할 수 있을 것이라고 생각합니다.

nullable

nullable은 Optional에 있는 nil을 허용한다는 것을 명시하기 위한 형수식자입니다.

예를 들면, NSDatainitWithContentsOfURL: 이니셜라이져는 아래와 같이 정의되어 있어서 리턴값이 nil이 될 수있다는 것을 명시하고 있습니다.

- (nullable instancetype)initWithContentsOfURL:(NSURL *)url;

이것을 스위프트에서 보면 아래와 같이 failable initializer로 변환되어 실패할 가능성이 있는 이니셜라이져가 되는 것을 알 수 있습니다.

public init?(contentsOfURL url: NSURL)

이와 같이 인스턴스 생성이랑 파라미터, 리턴값이 nil이 될 수 있는 경우에는 Objective-C쪽에 nullable을 지정해 두는 것으로 Swift에서 Optional으로 다루는 것이 가능해집니다.

Objective-C에서는 nullable을 지정하지 않는 경우에는 “Implicitly unwrapped optional”이 됩니다. 아래와 같은 Objective-C의 메소드에서 고려해보겠습니다.

- (UIImage*)createImage;

nullable을 붙이지 않고 Swift에서 사용하려고 하면 변환시에 “Implicitly unwrapped optional”이 되어 버립니다.

func createImage() -> UIImage!

이 메소드의 리턴 값을 사용하려고 할 때 nil일 경우 크래시가 발생해버려서 Swift부터 이 Objective-C의 코드를 사용할 때에 불안한 부분이 따라다니게 됩니다. 혹시 이러한 처리를 보게 된다면 nullable을 지정하여 Swift쪽에서 Optional으로 이용가능하도록 하면 Swift에서도 안심하고 이용할 수 있게 됩니다.

- (nullable UIImage*)createImage;

아래의 Objective-C의 코드를 수정하면 Swift에서는

func createImage() -> UIImage?

과 같이 변환됩니다.

nonnull

nonnull은 Optional이 아니라는 것을 명시하기 위한 형수식자로 nonnull을 함수랑 메소드의 파라미터, 리턴 값에 지정하는 경우에는 Optional한 변수를 지정하는 것이 불가능해집니다.

NS_ASSUME_NONNULL_BEGIN, NS_ASSUME_NONNULL_END

하지만 UIKit의 소스코드를 확인해보면 nonnull을 찾을 수 없습니다.

예를 들면 UIVisualEffectView는 이하와 같이 정의 되어 있습니다만 nonnull은 어디에도 쓰이고 있지 않습니다. nullable은 제대로 쓰이고 있습니다.

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIVisualEffectView : UIView <NSSecureCoding>
@property (nonatomic, strong, readonly) UIView *contentView; // Do not add subviews directly to UIVisualEffectView, use this view instead.
@property (nonatomic, copy, nullable) UIVisualEffect *effect;
- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end

하지만 Swift의 변환은 제대로 Optional이 아니도록 되어 있습니다.

@available(iOS 8.0, *)
public class UIVisualEffectView : UIView, NSSecureCoding {
    public var contentView: UIView { get } // Do not add subviews directly to UIVisualEffectView, use this view instead.
    @NSCopying public var effect: UIVisualEffect?
    public init(effect: UIVisualEffect?)
    public init?(coder aDecoder: NSCoder)
}

nonnull의 지정은 제대로 되어 있다는 것입니다. 왜 nonnull이 지정되지 않은것과 상관없이 변환이 가능한 걸까요?

알아보니 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 매크로가 사용되어 있었습니다.

앞에서 이야기 했던 UIVisualEffectView.h에도 제대로 이 매크로가 사용되어 있었습니다.

//
//  UIVisualEffectView.h
//  UIKit
//
//  Copyright (c) 2014-2015 Apple Inc. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

// ...중략

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIVisualEffectView : UIView <NSSecureCoding>
@property (nonatomic, strong, readonly) UIView *contentView; // Do not add subviews directly to UIVisualEffectView, use this view instead.
@property (nonatomic, copy, nullable) UIVisualEffect *effect;
- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end

NS_ASSUME_NONNULL_END

Foundation과 UIKit의 소스를 찾아보면 이 매크로가 사용되어 있었습니다.

확실히 nonnull, nullable을 하나하나 다 쓰고 있는 것은 큰일이겠지요. Objective-C에서 nonnull, nullable을 붙일 필요가 있는 경우는 적극적으로 NSASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END을 사용하고 nullable만을 쓰는 것이 좋을지도 모르겠습니다.

주의

같은 파일내에 메소드와 프로퍼티에 하나라도 nonnull, nullable을 쓰는 경우, 파일 내의 보든 메소드의 파라미터, 리턴 값, 프로퍼티에 형수식자를 붙이지 않으면 안됩니다. warning이 발생합니다.

 

Lightweight Generics

Swift로부터 Objective-C의 코드를 사용하려고 하는 때, NSArray로부터 변환과 NSDictionary로부터 Dictionary의 변환에서는 곤란할 부분이 없습니다만 배열 요소의 형이 AnyObject가 되어버려 곤란해 질 수 있을 것이라 생각합니다.

이와 같은 경우에 guard랑 Optional Binding같은 것을 사용해서 안전하게 형변환하여 구현할 것이라 생각하지만 Objective-C의 코드를 Generics를 사용해서 수정하는 편이 더 좋아보입니다.

UIView는 subviews라 불리우는 NSArray의 프로퍼티를 가지고 있습니다만, Generics를 사용해서 UIView의 배열이라는 것을 명시하고 있습니다.

@property(nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;

__kindof는 서브클래스(자식 클래스)도 허용하기 위한 어노테이션입니다. subviews는 UIView의 서브클래스도 허용하는 것을 명시합니다.

Objective-C에 Nullability와 Generics을 지정해 가는 공정

Objective-C에서 이하와 같이 정의되어있는 오브젝트를 보겠습니다.

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nonatomic, copy) NSArray *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end

이것을 Swift로부터도 사용하기 쉽도록 Nullability와 Generics를 지정해보겠습니다.

아무 손도 대지 않은 경우, Swift에서는 이렇게 보입니다. 이니셜라이져와 프로퍼티에 !가 붙어서 “Implicitly unwrapped optional”이 된 것을 알 수 있습니다.

public class MNPerson : NSObject {
    public var name: String!
    public var age: UInt
    public var items: [AnyObject]!
    public func hey() -> String!
    public init!(name: String!, age: UInt)
}

Objective-C의 헤더가 Swift에서 어떤식으로 표시되는지를 확인하는 방법

Objective-C에서 Nullability와 Generics를 지정할 때, jump bar의 좌측 끝에 있는 버튼을 클릭하면 나타나는 “Generated Interface”를 사용해서 Objective-C의 헤더파일이 Swift에 어떤 인터페이스가 되는지 확인하는 것이 가능합니다.

 

nullable의 설정

먼저 nullable을 붙여보도록 합시다. 아래와 같이 됩니다.

@interface MNPerson : NSObject
@property (nullable, nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (nullable NSString*)hey;
- (nullable instancetype)initWithName:(nullable NSString*)name age:(NSUInteger)age;
@end

Nullability는 포인터형만 지정하는 것이므로 primitive한 값, NSUInteger같은 것에는 nullable을 붙일 필요가 없습니다.

이것을 Generated Interface에서 보면 아래와 같이 됩니다.

public class MNPerson : NSObject {
    public var name: String?
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String?
    public init?(name: String?, age: UInt)
}

nonnull의 설정

우선 Optional으로 취급되고 있도록 되었습니다. 하지만 모든 것이 Optional이라면 하나하나 Optional Binding으로 값을 끄집어내지 않으면 안되기 때문에 좀 귀찮습니다.

nonnull으로 취급하는 장소가 없나 구현을 확인해봅시다.

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
    self = [super init];

    if (self) {
        _name = name;
        _age = age;
    }
    return self;
}


- (NSString*)hey {
    return @"hey";
}

이니셜라이져에서 인스턴스 변수인 _name_age에 값이 설정되어 있습니다. hey 메소드도 실패 가능성은 없기 때문에 여기에 해당하는 프로퍼티랑 메소드의 파라미터를 nonnull으로 해봅시다.

@interface MNPerson : NSObject
@property (nonnull, nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (nonnull NSString*)hey;
- (nonnull instancetype)initWithName:(nonnull NSString*)name age:(NSUInteger)age;
@end

Swift에서 봐보면 이렇게 됩니다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String?
    public init(name: String, age: UInt)

NS_ASSUME_NONNULL_BEGIN,NS_ASSUME_NONNULL_END 매크로를 사용하면 아래와 같이 쓸 수 있습니다.

NS_ASSUME_NONNULL_BEGIN

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end
NS_ASSUME_NONNULL_END

nil이 될 가능성이 있는 곳에만 nullable을 지정할 필요가 있습니다만 nonnull인 프로퍼티에 대하여는 지정이 불필요해집니다.

결과는 앞과 같은 형태, 아래와 같이 됩니다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

Generics의 설정

이것으로 Optional의 설정은 완료했습니다만 Swift로부터 사용할 때에 귀찮은 점이 한 부분 남아 있습니다. Generated Interface를 봐 봅시다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

items 프로퍼티가 AnyObject의 배열이 되어 있습니다. 하나하나 캐스트 하는 것도 귀찮습니다. 여기서는 items에 쌓여있는 것은 문자열이라 한정해서 Generics의 설정을 하겠습니다.

NS_ASSUME_NONNULL_BEGIN

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray<NSString*> *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end
NS_ASSUME_NONNULL_END

items 프로퍼티에 대하여 NSString을 지정했습니다. Generated Interface를 봐 봅시다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [String]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

위와 같이 [String]으로 되어 있는 것을 알 수 있습니다.

이렇듯 구현을 확인하면서 Swift로부터 이용하기 쉽도록 해 가는 것이 가능합니다.

정리

코드양적으로 봤을 때 그렇게까지 많이 재작성하지 않더라도 Swift에서 사용하기 쉽게 인터페이스를 수정하는 것이 가능합니다.

어떻게 구현되었는지 파악되어 있는 경우에는 이와 같은 Nullability와 Generics를 지정하는 것이 기존의 Objective-C의 코드를 Swift부터 사용하기 쉽게 할 수 있다는 것에 틀린 부분은 없다고 생각합니다.

단지, 이것들을 바꾸는 것은 이외로 간단하게 되는 것은 아닌 것 같은 인상을 줍니다. 이유라고 한다면 한 부분에만 nullablenonnull을 지정하는 것이 불가능하다거나 나름대로 큰 클래스가 된다면 nullable이라거나 nonnull인 것을 간단하게 판단할 수 없는 경우가 많아진다는 인상을 주기 때문입니다.

Objective-C의 경우 기본적으로는 nullable이 된다고 생각하지만, Swift에서 이용할 때에는 Optional로써 취급되지 않으면 안되기 떄문에 단순하게 nullable로 바꾸는 것에는 큰 강점을 느낄 수 없습니다.

그래도 Optional로써 취급되어지는 것이 강점이라고 생각하며 AnyObject의 캐스팅이 줄어든다는 것은 Swift코드를 작성해나가는데 큰 강점이 될 것이라 생각합니다.

참 고

1월 082017
 

알림
이 글은 Qiita에 게시된 “Modern Objective-C ビフォーアフター”(http://qiita.com/makoto_kw/items/d86fada0e38e9245912a , 2014-04-16 수정본 기준), makoto_kw님이 작성한 글을 번역한 것입니다.

Objective-C 언어는 2017년 현재도 여전히 많은 수의 사용자가 사용하고 있는 언어이다[1]. 최근 Objective-C의 관심이 Swift의 영향으로 근래의 웹에서 최근 자료를 찾기 힘든 것이 사실이다. 이 자료는 꽤 오래된 것이지만 회사에서 코드 리팩토링 업무를 하면서 modern objective-c 형식으로 변경이 필요하여 참조한 자료이다. 이를 사용하면 기존 방식에 비해 코드의 길이를 많이 줄일 수 있으며 불필요한 선언 코드를 줄일 수 있다는 것을 알게될 것이다. 물론 이 방법이 나온지 꽤 오래되었기 때문에 아마 대부분의 Objective-C 개발자들은 알고 있을 것으로 예상되지만 아직 이 사실에 대하여 알지 못하는 분들을 위해 도움이 될 수 있었으면 좋겠다.

Adopting Modern Objective-C

instancetype

instancetype 를 사용하면 컴파일러가 타입 체크가 가능함

@interface MyObject
- (id)myFactoryMethod;
@end

@interface MyObject
- (instancetype)myFactoryMethod;
@end

Enumeration Macros

“iOS6 SDK”부터 추가된 매크로

enum {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle
};
typedef NSInteger UITableViewCellStyle;

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle
};

bitmask는 NS_OPTIONS 를 사용

enum {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};typedef NSUInteger UIViewAutoresizing;

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5};

Migrating to Modern Objective-C

Importing Headers

#import <Foundation/NSObject.h>
#import <Foundation/NSString.h> //or @class NSString;

#import <Foundation/Foundation.h>

Accessor Methods

항상 엑세서 메소드를 사용하라. 단, initializer랑 dealloc안은 피한다.

- (void)myMethod {
    // ...
    [self setTitle:[NSString stringWithFormat:@"Area: %1.2f", [self area]]];
    // ...
}

- (void)myMethod {
    // ...
    self.title = [NSString stringWithFormat:@"Area: %1.2f", self.area];
    // ...
}

Memory Management

ARC를 사용한다. ARC은 Xcode 4.2(LLVM compiler 3.0)부터 지원한다. 일부기능은 컴파일러 만으로 해결이 안되기 때문에 iOS5 이상을 지원해야 한다.

NSMutableArray *array = [[NSMutableArray alloc] init];
// Use the array
[array release];

// or 

NSMutableArray *array = [[[NSMutableArray alloc] init] autoelease];
// Use the array

NSMutableArray *array = [NSMutableArray array];
// Use the array

id heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// ...
[heisenObject doSomething];
[heisenObject release];
// ...

id heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// ...
[heisenObject doSomething];
// ...

- (void)takeLastNameFrom:(Person *)person {
    NSString *oldLastname = [[self lastName] retain];
    [self setLastName:[person lastName]];
    NSLog(@"Lastname changed from %@ to %@", oldLastname, [self lastName]);
    [oldLastName release];
}

- (void)takeLastNameFrom:(Person *)person {
    NSString *oldLastname = [self lastName];
    [self setLastName:[person lastName]];
    NSLog(@"Lastname changed from %@ to %@", oldLastname, [self lastName]);
}

CFUUIDRef cfUUID = CFUUIDCreate(NULL);
NSString *noteUUID = (NSString *)CFUUIDCreateString(NULL, cfUUID);
CFRelease(cfUUID);

CFUUIDRef cfUUID = CFUUIDCreate(NULL);
NSString *noteUUID = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, cfUUID);
CFRelease(cfUUID);

Properties

property선언 및 구현에서는 synthesize를 사용한다. 인스턴스 변수에 직접 액세스를 하고 싶을 때에는 @synthesize title = _title;로 하여 _title 변수를 사용한다. 이 때 _title 은 선언하지 않더라도 컴파일러가 처리해 준다. 또한, @synthesize 자체를 생략하는 것도 무방하다(Xcode 4.4 – Apple LLVM 4.0 Compiler부터 지원).

한 가지 생각할 점으로, 변수 _title를 직접 참조용, 프로퍼티 self.title = ...(setter)를 값을 변경용으로 사용할 수 있음(initializer와 dealloc는 피함)

@interface Thing : NSObject {
  NSString *title;
}
- (NSString *)title;
- (void)setTitle:(NSString *)newTitle;
@end

@implementation Thing
- (NSString *)title {
    return title;
}
- (void)setTitle:(NSString *)newTitle {
    if (title != newTitle) {
        [title release];
        title = [newTitle copy];
    }
}
@end

@interface Thing : NSObject 
@property (copy) NSString *title;
@end

@implementation Thing
@synthesize title;
@end

@interface Thing : NSObject {
    float radius;
}
- (float)radius;
- (void)setRadius:(float)newValue;
- (float)area;
@end

@implementation Thing
- (float)radius {
    return radius;
}
- (void)setRadius:(float)newValue {
    radius = newValue;
}
- (float)area {
    return M_PI * pow(radius, 2.0);
}
@end

@interface Thing : NSObject
@property float radius;
@property (readonly, nonatomic) float area;
@end

@implementation Thing
@synthesize radius;
- (float)area {
    return M_PI * pow(self.radius, 2.0)
;}
@end

@interface Thing : NSObject {
    id delegate;
}
- (id)delegate;
- (void)setDelegate:(id)newDelegate;
@end

@implementation Thing
- (id)delegate {
    return delegate;
}
- (void)setDelegate:(id)newDelegate {
    delegate = newDelegate;
}
@end

@interface Thing : NSObject
@property (weak) id delegate;
@end

@implementation Thing
@synthesize delegate;
@end

Private State

Private 변수는 클래스 익스텐션 내에 property로 선언한다. 메소드도 마찬가지로 클래스 익스텐션을 사용한다.

@interface Thing {
   BOOL privateTest;
}

@interface Thing ()
@property BOOL privateTest;
@end
// ...

@interface Thing (PrivateMethods)
- (void)doSomethingPrivate;
- (void)doSomethingElsePrivate;
@end

@interface Thing ()
- (void)doSomethingPrivate;
- (void)doSomethingElsePrivate;
@end

Outlets

메모리 관리를 위해 strong, weak로 property를 선언한다.

@interface MyViewController : MySuperclass {
    IBOutlet ElementClass *uiElement;
}
@end

@interface MyViewController : MySuperclass
@property (weak) IBOutlet ElementClass *uiElement;
@end

Initializer Methods and dealloc

초기화와 해제는 액세서를 쓰지 않고 변수로 한다.

- (id)init {
    if (self = [super init]) {
        [self setTitle:@"default"];
    }
    return self;
}

- (id)init {
    self = [super init];
    if (self) {
        _title = @"default";
    }
    return self;
}

- (void)dealloc {
    [self setTitle:nil];
    [super dealloc];
}

- (void)dealloc {
    [_title release];
    [super dealloc];
}

Protocols

필수는 아닌 구현하지 않은 메소드가 있기 때문에 id를 그대로 사용하지 말고 optional선언을 사용해서 id형에 제대로 protocol을 지정한다.

@ButlerProtocol
- (void)makeTea;
- (void)serveSandwiches;
- (void)mowTheLawn;
@end

@protocol ButlerProtocol <NSObject>
- (void)makeTea;
- (void)serveSandwiches;
@optional
- (void)mowTheLawn;
@end
- (id <ButlerProtocol>)butler;

@property id <ButlerProtocol> butler;

Collections and Literals

@ 리터럴 구문과 [] 참조구문은 Xcode 4.4(Apple LLVM 4.0 Compiler)부터 지원했으나 iOS에 사용할 수 있게 된건 Xcode4.5 부터이다.

NSNumber *aNumber = [NSNumber numberWithFloat:2.3];

NSNumber *aNumber = @2.3f;

NSNumber *anotherNumber = [NSNumber numberWithFloat:x];

NSNumber *anotherNumber = @(x);

NSArray *anArray = [NSArray arrayWithObjects:aThing, @"A String",
                       [NSNumber numberWithFloat:3.14], nil];

NSArray *anArray = @[ aThing, @"A String", @3.14 ];

NSDictionary *aDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                value, @"Key",
                                [NSNumber numberWithBOOL:YES], @"OtherKey",                                nil];

objectivec:afterNSDictionary *aDictionary = @{ @"Key" : value, @"OtherKey" : @YES };

NSDictionary *distanceDict = [NSDictionary dictionaryWithObjectsAndKeys:            [NSNumber numberWithDouble:  0.0], kCIAttributeMin,
            [NSNumber numberWithDouble:  1.0], kCIAttributeMax,
            [NSNumber numberWithDouble:  0.0], kCIAttributeSliderMin,
            [NSNumber numberWithDouble:  0.7], kCIAttributeSliderMax,
            [NSNumber numberWithDouble:  0.2], kCIAttributeDefault,
            [NSNumber numberWithDouble:  0.0], kCIAttributeIdentity,
            kCIAttributeTypeScalar,            kCIAttributeType,
            nil];

NSDictionary *distanceDict = @{
        kCIAttributeMin       : @0.0,
        kCIAttributeMax       : @1.0,
        kCIAttributeSliderMin : @0.0,
        kCIAttributeSliderMax : @0.7,
        kCIAttributeDefault   : @0.2,
        kCIAttributeIdentity  : @0.0,
        kCIAttributeType      : kCIAttributeTypeScalar
};

NSDictionary *slopeDict = [NSDictionary dictionaryWithObjectsAndKeys:            [NSNumber numberWithDouble: -0.01], kCIAttributeSliderMin,
            [NSNumber numberWithDouble:  0.01], kCIAttributeSliderMax,
            [NSNumber numberWithDouble:  0.00], kCIAttributeDefault,
            [NSNumber numberWithDouble:  0.00], kCIAttributeIdentity,
            kCIAttributeTypeScalar,             kCIAttributeType,
            nil];

NSDictionary *slopeDict = @{
        kCIAttributeSliderMin : @-0.01,
        kCIAttributeSliderMax : @0.01,
        kCIAttributeDefault   : @0.00,
        kCIAttributeIdentity  : @0.00,
        kCIAttributeType      : kCIAttributeTypeScalar };

id firstElement = [anArray objectAtIndex:0];
[anArray replaceObjectAtIndex:0 withObject:newValue];

id firstElement = anArray[0];
anArray[0] = newValue;

id value = [aDictionary objectForKey:@"key"];
[aDictionary setObject:newValue forKey:@"key"];

id value = aDictionary[@"key"];
aDictionary[@"key"] = newValue;

NSArray *array = ...;
int i;
for (i = 0; i < [array count]; i++) {
    id element = [array objectAtIndex:i];
    // ... 
}

NSArray *array = ...;
for (id element in array) {
     // ...
}

Blocks

NSArray

정렬은 (NSArray *)sortedArrayUsingComparator:(NSComparator)cmptr를 사용할 수 있다.

NSArray *array = ...;
NSArray *sortedArray;
sortedArray = [array sortedArrayUsingFunction:MySort context:NULL];

NSInteger MySort(id num1, id num2, void *context) {
    NSComparisonResult result;
    // Do comparison
    return result;
}

NSArray *array = ...;
BOOL reverse = ...;
NSArray *sortedArray;
sortedArray = [array sortedArrayUsingComparator:^(id num1, id num2) {
    NSComparisonResult result;
    // Do comparison
    return result;
}];

each는 (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block을 사용할 수 있다.

NSArray *array = ...;
for (id element in array) {
    // ... 
}

NSArray *array = ...;
[array enumerateObjectsUsingBlock:
      ^(id obj, NSUInteger idx, BOOL *stop) {
      // ...
      NSLog(@"Processing %@ at index %d”, obj, idx);
      // ...
}];

NSDictionary

(void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block을 사용할 수 있다.

NSDictionary *dictionary = ...;
for (NSString *key in dictionary) {
    id object = [dictionary objectForKey:key];
    // Do things with key and object.
}

NSDictionary *dictionary = ...;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) {
    // Do things with key and object.
}];

Notifications

(id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block을 사용할 수 있다.

- (void)registerForNotifications {
    NSNotificationCenter *center = ...
    [center addObserver:self
        selector:@selector(windowBecameKey:)
        name:NSWindowDidBecomeKeyNotification
        object:self.window];
}
// Different context
// No queue information
- (void)windowBecameKey:(NSNotification *)notification {
    // Get contextual information.
}

- (void)registerForNotifications {
    NSNotificationCenter *center = ...
    MyClass *__weak weakSelf = self;
    [center addObserverForName:NSWindowDidBecomeKeyNotification
        object:self.window
        queue:[NSOperationQueue mainQueue]
        usingBlock:^(NSNotification *) {
            // ...
            [weakSelf doSomething];
            // ...
    }];
}

Blosks내에서 self는 직접 참조 되지 않고 약한참조 MyClass *__weak으로 변환해서 사용해야 한다.

기타

  • 원래 자료는 Mac Developer Library modern으로 검색
  • 사용하고 싶은 메소드가 블럭에 대응되지 않는 경우 → BlocksKit의 이용을 검토
  • Xcode(컴파일러) 업데이트 정보는 What’s New in Xcode를 참고
  • Xcode에 modern Objective-C로 변환하는 기능을 제공함 (Edit > Covert > To Modern Objective-C Syntax… 사용. Xcode8.2.1 기준)

참고자료

[1] TIOBE Index for December 2016(2017-01-08 04:12 KST 확인), http://www.tiobe.com/tiobe-index/

9월 142016
 

VPN은 외부로부터 격리된 인트라넷을 구성하면 외부에서 접속이 불가능한데 이를 가능하게 해주는 서비스이다. 물리적으로 서로 떨어져 있는 회사 네트워크를 외부에 공개하지 않으면서 통신하기 위해 만들어 졌다. 물론 회사 네트워크에서 사용하면 이런용도로 사용하게 되겠지만, 집에서 공유기 뒤에 NAS를 쓰거나, 개인 서버를 사용하는 경우에도 동일한 방법으로 활용할 수 있다. 만약 NAS나 개인서버를 SMB(네트워크 공유 기능)통해 사용하기 위해서는 같은 네트워크에 있어야 한다. 하지만 외부에서는 이 방법을 사용할 수 없는데, 이는 ISP(네트워크 제공사)의 방화벽 문제가 있기 때문이며, 따라서 WebDAV등을 이용하여 구현하는 방법이 일반적이다. 이 경우에 외부에는 내부 서버를 공개하지 않으면서 안전(암호화된)하게 내부 네트워크에 접속된 것과 같은 상태를 만들 수 있는 방법이 바로 VPN인 것이다.

일반적인 공유기는 VPN 프로토콜 중에 하나인 PPTP를 지원하도록 구현되어 있다. 하지만 PPTP의 문제는 인증시스템이나 암호화 방식이 매우 오래되었으며 보안에 취약하다는 점이다[1]. 그래서인지 이번에 업데이트 되는 iOS 10, macOS Sierra부터는 보안상의 이유로 더 이상 PPTP프로토콜 기반의 VPN서비스를 이용할 수 없게 되었다[2]. 하지만 필자가 가지고 있는 공유기에서 PPTP를 지원하지 않기 때문에 다른 프로토콜을 사용하는 VPN이 필요하게 되었다. 하지만 굳이 공유기에서 PPTP가 아닌 다른 프로토콜을 지원하게 하려면 공유기의 펌웨어를 커스터마이징해야 한다는 것인데 그건 너무 시간이 많이 걸리며 공유기 공급사에서 소스코드를 공개하지 않는다는 점에서 어려움이 있다. 따라서 VPN서버를 공유기에서 내부 서버로 변경하면 여러종류의 VPN프로토콜이 사용 가능해 지기 때문에 홈 서버에 VPN서버를 설치 하게 되었다. 좀 더 큰 CPU파워를 사용할 수 있게 됨으로써 최신 VPN 프로토콜과 고급 암호화 기능도 사용할 수 있게 되었다.

자료 조사를 해보면서 여러 종류의 VPN 프로토콜이 존재함을 알 수 있었으며, VPN 프로토콜을 선택 기준이 필요해 졌는데 모바일이나 OS에서 추가적인 프로그램 설치 없이 사용할 수 있어야 한다는 조건을 걸었다. 이 조건을 만족하면 모바일로 외부에서 집에 있는 서버에 있는 동영상을 스트리밍으로 볼 수 있기 때문에다. 이에 추가 클라이언트 환경 설치가 필요한 OpenVPN은 제외되었다. 그렇게 되면 가능한 프로토콜이 L2TP/IPSec, IPSec, IKEv2 를 사용할 수 있음을 확인 가능하였다. 최초에는 L2TP/IPSec을 이용할 예정이였으나, 필자가 사용하는 OS에서는 기본 지원하며, 속도도 빠르고 높은 보안을 제공하는 IKEv2 프로토콜을 이용하여 구현하기로 하였다[3].

IKEv2 프로토콜을 사용할 때 ESP를 통하여 암호화된 패킷을 전송한다. 원래 ESP는 IP프로토콜에 바로 들어가기 때문에 공유기(NAT)뒤에 VPN서버가 있는 경우에는 DMZ설정 같은 것이 필요하며 공유기가 이 기능을 지원해야할 수도 있다. DMZ로 설정하면 VPN서버의 모든 부분이 인터넷으로 공개되기 때문에 내부를 숨기겠다는 목적이 달성되지 않는다. 하지만 선배 개발자들은 이런 문제를 해결하기 위해 NAT Passthrough라는 것을 제공하며 UDP프로토콜에 ESP를 담아서 보내도록 구현이 된다. 이 문제는 이를 설정함으로써 해결 된다.

준비물은 MITM(Man in the middle attack)방지를 위한 서버 인증을 위한 인증서가 필요하다. 이 글에서는 self-signed 인증서를 사용하여 IKEv2서버 구축하는 방법을 알려주지 않는다. 만약 self-signed 인증서를 사용하게 되면 클라이언트에서 추가적인 설정이 필요할 수도 있다.

이 글은 공유기의 IP주소로 DNS/DDNS 등의 방법으로 도메인이 할당되어 있으며, 이 도메인으로 정상적인 SSL 인증서를 발급받았다는 전제로 작성되어 있다.

SSL 인증서는 일반적으로 유료이지만 StartSSL이나 Let’s Encrypt 등을 통하여 무료로 발급 가능하다.

IKEv2를 동작시키기 위해서는 UDP/500과 UDP/4500을 열어 두어야 한다. UDP/500은 IKE프로토콜을 위해 필요하며, UDP/4500은 IPSec을 이용하기위해 필요하다. 이 두가지 포트를 공유기에서 포트포워딩 설정을 해두어야 한다.

IKEv2를 사용하기 위해서는 strongswan이라는 패키지 설치와 암호화 기능 등의 기능을 사용하기 위해다.

1. 패키지 설치
apt-get install strongswan libcharon-extra-plugins

2. 인증서 설정

2.1. root 인증서 복사
발급받은 인증서의 루트 인증서 및 채인 인증서 복사
복사시 한 파일당 하나의 인증서만 포함 시킬 것
/etc/ipsec.d/cacerts

2.2. 인증서 복사
발급받은 인증서 파일 복사(pem)
/etc/ipsec.d/certs

2.3 인증서의 비밀키 복사
발급받을 때 사용한 인증서 비밀키 복사 (키 패스워드 제거할 것)
/etc/ipsec.d/private

2.4 권한 설정
chmod 740 /etc/ipsec.d/cacerts
chmod 740 /etc/ipsec.d/certs
chmod 700 /etc/ipsec.d/private

3. /etc/ipsec.conf 파일 수정

config setup
  strictcrlpolicy=yes
  uniqueids = no

conn roadwarrior
  auto=add
  compress=no
  type=tunnel     # tunnel: network 계층(ip)부터 암호화, transport: 전송 계층(transport layer; TCP/UDP)부터 암호화
  keyexchange=ikev2
  rekey=no
  reauth=no
  fragmentation=yes
  forceencaps=yes
  dpdaction=clear
  dpddelay=35s
  dpdtimeout=2000s
  left=%any
  leftid=@example.com   # 아래 ipsec.secrets의 이름과 통일, 인증서에 도메인 포함되어 있을 것
  leftcert=example_com.pem   # 상기 certs 디렉토리에 복사한 인증서 파일명으로 변경 
  leftsendcert=always
  leftsubnet=0.0.0.0/0
  leftauth=pubkey
  right=%any
  rightid=%any
  rightauth=eap-mschapv2
  eap_identity=%identity
  rightdns=192.168.1.1           # 여기서 rightdns 의 주소를 VPN사용시 쓸 DNS서버 주소로 변경 필요하다. (예: 8.8.8.8)
  rightsourceip=10.8.10.0/24
  rightsendcert=never

4. /etc/ipsec.secrets

# 서버 도메인과 키 설정

# 예제 - 아래 도메인은 클라이언트 설정시 사용, 인증서에 아래 도메인이 포함되어 있어야 함, 상기 복사한 비밀키 파일명을 사용함
example.com : RSA “example_com.key"

{{VPN접속ID}} : EAP “{{VPN접속PASSWORD}}"
# 예제
admin : EAP “password"

5. 서버 재시작
ipsec restart

6. 공유기(NAT) 포트 포워딩
UDP 500 과 UDP 4500을 내부에 설치된 VPN서버로 포워딩

7. 테스트
iOS기기나 macOS에서 아래의 설정으로 만듦
내부망과 외부망에서 각각 수행하여 정상적으로 접속 되는지 확인한다.

  • VPN Type : IKEv2
  • Server Address(서버 주소) : VPN의 도메인 주소 (예: example.com)
  • Remote ID (리모트 ID) : 상기 적은 도메인 주소(예: example.com)
  • Local ID (로컬 ID) : 비워둘 것
  • Authentication Settings…(인증 설정) : 사용자 이름
    사용자 이름 : ipsec.secrets 에 설정한 아이디
    패스워드 : ipsec.secrets 에 설정한 패스워드
VPN 설정 예시

VPN 설정 예시

 

connected to VPN

VPN 접속 성공

8. 관련 접속 로그 확인
tail -f /var/log/syslog
tail -f /var/log/auth.log

이 글을 통해 우분투 서버에서 IKEv2를 사용한 서버 구축이 가능하였으며 이를 통해 어디에서나 모바일 디바이스, 데스크톱을 통하여 내부 네트워크에 접속할 수 있게 되었으며, 암호화가 되어 안전한 환경에서 내부서버의 데이터를 사용할 수 있게 되었다.

참고문헌

[1] B. Schneier, Mudge, “Cryptanalysis of Microsoft’s PPTP Authentication Extensions (MS-CHAPv2)”, CQRE ’99, Springer-Verlag, 1999, pp. 192-203.
[2] “Prepare for removal of PPTP VPN before you upgrade to iOS 10 and macOS Sierra”, Apple, https://support.apple.com/en-us/HT206844
[3]  https://hide.me/en/blog/2015/03/whats-the-difference-considering-pptp-vs-l2tp-vs-sstp-vs-ikev2/
[4] https://hub.zhovner.com/geek/universal-ikev2-server-configuration/