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월 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/

9월 102016
 

우리는 ubuntu에서 패키지 인덱스를 업데이트 하기 위해 apt-get update 라는 명령을 사용하여 업데이트한다. 하지만 만약 커스텀 패키지를 이용하는 경우 (예: owncloud)에 keyexpired라는 문구를 볼 수 있다. 이는 패키지를 서명하는데 사용된 인증키가 시간이 지나 만료되었기 때문이다. 따라서 키를 다시 업데이트 해주는 작업이 필요하다. 아래의 절차를 따라 새로 받아보도록 하자.

1)  아래와 같은 메시지를  apt-get update 사용시 발견

Reading package lists... Done
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://download.opensuse.org  Release: The following signatures were invalid: KEYEXPIRED 1472205884

W: Failed to fetch http://download.opensuse.org/repositories/isv:/ownCloud:/community/xUbuntu_14.04/Release

W: Some index files failed to download. They have been ignored, or old ones used instead.

2) apt-key list 명령어를 통해 expired 키를 찾기

# apt-key list | grep "expired:"
pub 1024D/BA684223 2012-02-08 [expired: 2016-08-26]

3) 목록에 있는 내용을 통해 “BA684223″이란 키가 2016-08-26에 만료되었으며 업데이트 해야함을 확인 가능

4) apt-key 명령으로 새로 키(BA684223)를 업데이트 받음

# apt-key adv --recv-keys --keyserver keys.gnupg.net BA684223

# apt-key adv --recv-keys --keyserver keys.gnupg.net BA684223
Executing: gpg --ignore-time-conflict --no-options --no-default-keyring --homedir /tmp/tmp.DE4JfiHL45 --no-auto-check-trustdb --trust-model always --keyring /etc/apt/trusted.gpg --primary-keyring /etc/apt/trusted.gpg --keyring /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg --keyring /etc/apt/trusted.gpg.d/docker-maint-testing.gpg --keyring /etc/apt/trusted.gpg.d/openjdk-r-ppa.gpg --keyring /etc/apt/trusted.gpg.d/webupd8team-java.gpg --recv-keys --keyserver keys.gnupg.net BA684223
gpg: requesting key BA684223 from hkp server keys.gnupg.net
gpg: key BA684223: "isv:ownCloud OBS Project <isv:ownCloud@build.opensuse.org>" 5 new signatures
gpg: Total number processed: 1
gpg:         new signatures: 5

5) 다시 우분투 패키지 업데이트 작업을 진행한다.

# apt-get update

# apt-get upgrade

 

참고 자료

[1] http://superuser.com/questions/513609/how-to-apt-update-when-apt-is-not-accepting-the-repository

7월 312016
 

머신러닝이라는 과목에 대하여 어떻게 하면 공부를 효율적으로 즐겁게 할 수 있을지를 고민하던 중 “코세라”에 대하여 알게 되었다. 코세라는 컴퓨터과학과 교수인 Andrew Ng과  Daphne Koller가 공동 창업하여, 비싼 등록금을 내지 못하고 교육의 기회를 가지지 못하는 사람들을 위해 공짜로 온라인 수업을 제공하는 서비스이다[1]. 코세라에서 다양한 머신러닝 수업이 열려있는 것을 확인할 수 있었는데, 이 중 가장 빨리 시작하면서 강의가 길지 않고 요점만 들을 수 있는 수업을 찾다가 창업자인 Andrew Ng교수가 직접 강의하는 ML수업을 선택하게 되었다.

Coursera ML - https://www.coursera.org/learn/machine-learning

Coursera ML – https://www.coursera.org/learn/machine-learning

코세라의 좋은 점은 첫 번째로 공간에 제약없이 공짜로 좋은 강의를 들을 수 있다는 점이다. 일반적으로 이러한 강의를 듣기 위해서는 지정된 시각에 직접 수업하는 장소에 가서 수업을 들어야 한다는 문제가 있다. 스타트업에 다니고 있는 본인은 평일에 시간을 낸다거나 하는 것은 거의 불가능 하다. 그렇기 때문에 온라인 강의는 나에게 매우 필요하였고 이를 선택한 것은 적절한 결정이였다. 또한 세계 유수 대학의 수업을 들을 수 있다는 부분도 고무적이였다. 두 번째로는 돈을 내면 수료증을 받을 수 있다는 점이다. 물론 수업을 듣는 것은 공짜지만, 수료증은 돈(수업당 USD 79)을 내야한다는 점이 안타까운 점인데 그 부분은 수료증을 필요로 하는 사람만 하면 될 것이다. 수료증을 링크드인에 자격증 항목에 등록이 가능하다는 점, 수료증을 대학교 학점으로 인정해주는 곳도 있는 만큼 돈을 낸 값어치는 한다고 생각한다. 세 번째는 수업을 같이 듣고 있는 사람들과 함께 스터디를 하거나 멘토를 통해 도움을 받을 수 있다는 점이다. 수업이 시작하는 기간이 정해져 있기 때문에 수업을 나 뿐만 아니라 다른 사람도 함께 시작하기 때문에 이러한 시스템이 가능한 듯 하다. 물론 영어를 잘 해야한다.

하지만 이에 반해 단점도 있는데, 이는 결국은 영어권 기반의 서비스라는 한계이다. 물론 수업의 자막 번역 기부를 하는 것이 가능하기 때문에 자막이 여러 언어로 번역 되어 있긴 하지만 기본적으로 퀴즈나 과제 문서는 영어로 되어 있기 때문에 영어를 모르고서는 수업을 듣는 것, 과제 하고 퀴즈를 푸는 것이 불가능하다. 포럼에 질문 글을 올리거나 스레드를 여는 것 조차도 영어가 필요하다는 점이 하나의 장벽이 된다. 이번에 들은 ML수업은 일어, 중어, 스페인어 등으로 번역이 완료되어 있으나 한국어는 최초 강의 하나만 번역되어있었다.

Coursera ML - 두 번째 주 강의 스크린샷

Coursera ML – 두 번째 주 강의 스크린샷

ML수업의 경우 동영상 수업, 퀴즈, 과제로 구성되어 있다. 위의 그림은 ML 수업 둘째 주 수업에 대한 스크린 샷이다. 총 11주의 수업으로 구성되어 있으며, 모든 과제 제출, 강의 듣기, 퀴즈 풀기(각각 80점 이상)를 하면 수료상태가 되어 수료증을 받을 수 있게된다. 물론 수료증 없이도 진행 가능하며, 나중에 필요하게 된다면 신청 가능하다. 단 퀴즈를 풀 때 과제를 푸는 사람이 본인이 맞는지 확인하기위해 확인 과정을 거치게 되는데 이 부분에 대해서 설정 및 진행을 해두어야만 한다는 점을 유의 해야 한다. 퀴즈를 풀기 전에 인증 없이 퀴즈를 풀면 수료증을 받을 수 없는 듯 하다.

자막의 언어를 설정할 수 있음

자막의 언어를 설정할 수 있음

동영상 수업을 들을 때 플레이어를 통해 자막을 설정할 수 있다. 물론 아래 쪽에 자막의 스크립트 전문을 볼 수있다. 수업 중간중간에 간단한 퀴즈를 푸는 것도 있는데, 잘 이해했는지 확인하기 위해서 나오는 듯 하며, 풀고 싶지 않으면 스킵하는 것도 가능하다. 딱히 성적에 반영되는 것 같지는 않다.

퀴즈의 경우 ML수업은 5문제 객관식 문제를 푸는 방식이였고 4문제 이상 맞추면 통과된다. 5지선다 형도 있고 맞는 것 체크하는 것이 있고 계산 문제도 있지만 수업만 열심히 들었으면 크게 푸는데 어려움은 없다. 만약 한 번에 통과하지 못하더라도 세 번까지 연속적으로 풀 수 있으며, 제일 점수가 잘 나온 것을 기준으로 성적이 업데이트 된다. 만약 세 번을 다 했을 경우 8시간 이후에 다시 기회가 주어진다. 만약 제한시간(보통 북태평양기준시(PDT) 일요일 23:59:59, 한국시간(KST)으로 월요일 4시)까지 통과하지 못하면 점수를 깎는 조건으로 일 주일 더 도전 가능하다. 문제와 답이 재도전 할 때 마다 조금씩 바뀌기 때문에 문제와 답안을 잘 읽고 풀어야 한다. 수료증 받기 위한 인증으로는 데스크탑의 경우 타자 패턴, 모바일의 경우 사진 인증을 사용한다. 상황에 맞게 하면 된다. 혹시 사정이 생겨서 수업을 못듣개 되더라도 나중에 다시 들으면 된다고 하니 이 부분에 대해서 걱정은 안해도 될 듯 하다.

과제의 경우 ML은 Matlab(Octave)프로그래밍을 한 후 답을 올리는 방식을 취한다. 처음 보는 언어라도 기본 프로그래밍 실력이 있다면 수업중에 어떻게 하면 되는지 설명만 듣는 것으로 과제 풀이가 가능하다. 계속해서 도전이 가능하며 그 중 점수 제일 높게 나온 것이 최종 성적이 된다. 그러니 중간 중간에 한 문제, 한 문제 풀 때 마다 등록하면 된다. 물론 과제 문서에도 그렇게 하도록 시킨다. 왜냐하면 앞의 문제를 풀어서 정상동작해야 그 다음 과정이 진행 가능한 경우가 많기 때문이다. Octave 3.8이상부터 지원된다고 되어 있지만 Octave 4.0 이상에서는 과제 등록이 안되는 경우가 있는데 이 경우 패치 프로그램을 받아야 한다. 이 부분은 추후 다른 포스팅을 통해 방법을 설명하도록 하겠다.

Octave-gui with ex2 in Coursera ML

Octave gui – 2번째 과제 관련 스크린샷

친절하게 문제 하나 풀 때마다 업로드 시점을 알려 준다. - 코세라 과제Ex1

친절하게 문제 하나 풀 때마다 업로드 시점을 알려 준다.

필자는 주말에 주로 수업을듣고 과제를 풀었으며 다음과 같은 방식으로 스터디를 진행하였다. 첫 번째 차수에 영어자막을 띄어놓고 일단 쭉 봐서 대충 내용 파악을 하였다. 두 번째 차수에서 이해 안되는 부분을 위주로 보고, 만약 이해가 전혀 안되면 처음부터 다시 쭉 보는 방식으로 진행하였다. 세 번째는 일어 자막 스크립트를 가지고 한 번 쭉 보거나, 상황에 따라 번역기를 사용하여 이해를 하는 방향으로 스터디 했다. 마지막으로 스크린샷을 뜨고 요약하여 노트를 만들었다. 과제를 하면서는 소스코드가 공개 되어 있기 때문에 어떻게 동작 되는지, 동작 코드를 전체적으로 쭉 훑어 보는 방식으로 진행하니 ML 구현에서 숨겨진 부분이 어떻게 동작하는지 이해할 수 있었다. 과제도 수업 듣는것과 비슷하게 첫 번째에는 쭉 풀어보고, 실패하면 다시 노트를 확인해서 부족한 부분을 스터디 하는 방식으로 부족한 부분을 채워 나갔다.

에버노트를 이용하여 정리

에버노트를 이용하여 정리 – 코세라 수업 스크린샷 발췌

11주가 지나 마지막 퀴즈를 풀면 아래 그림과 같은 축하 메시지가 나오고, 수료증이 발급 된다. 물론 링크드인으로 수료증을 보낼 수 있다. 11주간의 결실이 나온 것이다.

물론 이 수업 하나를 들었다고 해서 머신 러닝 전문가가 된다거나 하는 것은 아니다. 하지만 머신러닝이 어떤 것이고 어떤 곳에 사용할 수 있는가 어떤 부분을 고려해야하는가와 같은 질문에는 강의를 통해 충분히 답변이 된 것 같다. 응용하는 부분은 좀 더 공부가 필요하겠지만 공부방향을 잡는 것은 가능할 것 같다.

수업 완료 후 화면 스크린샷

수업 완료 후 화면 스크린샷

코세라 인증서 및 링크드인 자격증란 등록 스크린샷

코세라 인증서 및 링크드인 자격증란 등록 스크린샷

처음으로 영어로 된 수업을 들으면서 많은 시행 착오도 있었고 어려움도 있었지만 어떻게 해쳐 나온 것 같다. 모르는 부분이 있다거나 실제 시스템을 잘 알고 이해하기 위해서 영어로된 글을 읽는 것은 대학원에서 짜증날 정도로 했던 일이지만, 여전히 영어로된 글은 적응이 되지 않는 정말 피곤한 일인 것 같다. 간단하게 나마 필자가 경험했던 것을 정리해 두었는데 추후 코세라를 시작하는 분들에게 많은 도움이 되길 바란다. 필자는 앞으로 ML 이외의 수업에도 도전해볼 생각이다.

참고자료

  1. Coursera – Wikipedia Korean, https://ko.wikipedia.org/wiki/%EC%BD%94%EC%84%B8%EB%9D%BC
3월 102016
 

윈도우에서 “bundler install”할 때 아래와 같은 에러가 발생했을 때 해결방법

C:\Ruby22\bin\ruby.exe -e $stdout.sync=true;$stderr.sync=true;load($0=ARGV.shift) C:/Ruby22/bin/bundle install
Checksum of /versions does not match the checksum provided by server! Something
is wrong.

Process finished with exit code 17

의외로 간단하게 끝났다[1].

 

gem uninstall bundler
gem install bundler

번들러를 다시 깔아주면 해결되는 듯 하다.

 

참고문헌

[1] Rails initialization checksum error, Stackoverflow, http://stackoverflow.com/questions/35315712/rails-initialization-checksum-error