Hibernate Validator + Kotlin 안될때..

TL;DR

// not working
class TestEntity(
    @NotEmpty
    val testVal: String
)

// working
class TestEntity(
    @field:NotEmpty
    val testVal: String
)

// working
class TestEntity(
    @get:NotEmpty
    val testVal: String
)

Validation을 실행해도 Constraint에 걸리지 않는 문제가 있었다. 자바로 비슷한 코드 짜서 테스트해보니 잘되길래 아 코틀린에서 뭔가 문제가 있다는걸 알아내고 코틀린 문서를 자세히 읽어본 결과 annotation use-site target을 지정 할 수 있다는 것을 알았다. Kotlin의 클래스 프로퍼티는 자바의 여러개의 엘리먼트에 대응되기 때문에 (예를 들면 필드 하나를 만들면 코틀린이 getter, setter도 내부적으로 알아서 추가해주는?) annotation 별로 타겟을 지정하기 위한 것이다.

마츠모토 유키히로의 프로그래밍 언어 만들기

굉장히 친근한 화법으로 동네 카페에서 개발경력 화려하신 아저씨를 만나 얘기하는 느낌이다. 루비라는 언어를 왜 만들게 되었는지 부터 프로그래밍 언어를 디자인 하는 방법, 실제로 구현하는 방법까지 설명도 해준다. 어려운 내용이지만 굉장히 재밌다. 사실 루비라는 언어를 개인적으론 좋아하진 않지만, 프로그래밍 언어를 개발한 그 열정과 천재성, 끈기에 리스펙..!!

… 의욕이 충만했다. 나는 프로그래머의 3대 미덕의 하나인 오만을 충분히 유지한 인물이기 때문에, 만들려면 펄이나 파이썬에 지지 않는 언어를 만들겠다고 결심했다. 또, 그것이 가능할 거라 믿었다. 근거 없는 자신감은 무섭지만, 이러한 근거 없는 자신감이야말로 종종 동기 부여의 원천이 된다.

마츠모토 유키히로의 프로그래밍 언어 만들기 1장 어떤 언어를 만들까? 중…

조금 오만해져도 괜찮은걸까 싶다 ㅋㅋㅋ 자신감을 갖자.

안드로이드 앱 HTTPS 통신 캡처하기

어떤 앱을 크롤링(?) 해보고 싶어졌는데 나쁜짓 할려는건 아니고.. 아무튼 이게 웹사이트는 없고 앱만 있는 경우라 분석이 쉽지 않았다. 2시간 정도 삽질을 했는데 까먹을까봐 정리.

일단 HTTPS 통신을 가로채서 내용물을 까보려면 Man In The Middle Attack 방식을 쓰는게 좋다. 한마디로 프록시서버 구축하면 된다. A라는 서버랑 B라는 내 폰이랑 통신을 하는데 C라는 프록시서버가 끼어서 대충 A <-> C <-> B 이런 그림이다. C에서 우리는 HTTPS 통신의 내용물을 열어볼수 있다.

근데 자꾸 신뢰할 수 없는 인증서라고 SSL Handshake가 실패해서.. 프록시에서 사용하는 CA 인증서를 앱이 신뢰하도록 만들기 위하여 앱을 언패킹한후 약간 수정해줘야한다. 맥 기준으로 모든 과정을 정리해보도록 한다.

준비물

  1. Charles proxy: 유료인데 이거 진짜 편한듯. 무료 버전이라며 30분마다 꺼지지만.. 불편함을 감수하고 쓸만하다.
  2. 안드로이드 apktool

이거 두개를 먼저 설치하자.

과정

APK 파일 가져오기

  1. 먼저 본인의 핸드폰이나 에뮬레이터에서 열어볼 앱을 앱스토어를 통해 설치한다.
  2. adb로 앱의 패키지명(ddd.xxx.com 뭐 이런식..) 을 알아낸다.
  3. adb로 앱이 설치된 경로를 알아낸다.
  4. adb를 이용해 apk파일을 pc로 옮긴다.

2~4 번의 과정을 쉘에서 실행하면 아래와 같다.

# 디바이스에 설치된 패키지 리스팅
adb shell pm list packages

# 특정 앱의 설치된 경로 파악
adb shell pm path com.xxx.aaa

# 해당 경로의 파일 가져오기
adb pull /data/app/com.xxx.aaa/base.apk

APK 파일 수정

APK 파일 언패킹

apktool d xxx.apk

언패킹된 디렉토리의 res/xml 경로에 network_security.xml 추가

<network-security-config>  
      <base-config>  
            <trust-anchors>  
                <!-- Trust preinstalled CAs -->  
                <certificates src="system" />  
                <!-- Additionally trust user added CAs -->  
                <certificates src="user" />  
           </trust-anchors>  
      </base-config>  
 </network-security-config>

언패킹된 디렉토리의 AndroidManifest.xml 수정

<application> element 를 찾아서 android:networkSecurityConfig="@xml/network_security_config" attribute를 추가해준다. 이 과정이 뭔지 간략히 설명하면, 후술할 charles proxy의 CA 인증서를 앱에서 신뢰하도록 만드는 것이다.

그리고 다시 apk파일로 패킹

apktool b xxx -o xxx-modified.apk

그 후에 adb를 이용해 apk 파일을 디바이스에 설치하려고 하면 사인되지 않은 apk 파일이라며 설치가 되지 않는다. 사인해주자

keytool -genkey -alias test -keyalg RSA -validity 20000 -keystore test

jarsigner -verbose -keystore test xxx-modified.apk test

그 다음, adb로 디바이스에 설치 (그 전에 앱스토어를 통해 설치했던 기존 앱은 삭제해주자)

adb install xxx-modified.apk

PROXY 세팅하기

Charles proxy를 설치한다.

그다음 Proxy -> SSL Proxy settings 메뉴로 들어가자.

이렇게 와일드 카드로 설정해준다.

그 다음 이 프록시에 연결 할 디바이스에서 network settings에 들어가서 proxy 를 설정한다. charles proxy 에서 help -> Local IP Address 에 들어가면 아이피를 확인할 수 있다. 해당 아이피를 적고 포트는 8888로 설정한다.

그 다음, 디바이스에서 브라우저를 열어서 charlesproxy.com/getssl 으로 들어간다. 여기서 키 파일 하나가 자동으로 다운로드 받아지는데, 이건 charlesproxy 에서 사용하는 CA 인증서로서 폰에 설치하도록 하자. (proxy를 통해서 들어가야만 받을 수 있다)

드디어!

아까 다시 패킹해서 설치한 앱을 실행하고 charlesproxy에서 잘 잡히는지 확인하면 된다. 참 쉽죠?

Trie 자료구조

얼마전 요즘 핫한 모 기업의 전화면접에서 광탈한 후 당시 받았던 질문이 문득 다시 떠올랐다. 사전을 구현해야 한다면 어떤 자료구조로 구현을 할거냐는 질문에 해쉬맵을 쓸거라고 답했고.. 면접관은 그럼 일부만이라도 포함된 단어를 정렬하여 리스팅 할려면 어떤 구조로 짤건지 질문했다. 여기서 나는 머리속이 하얘지면서 아무 생각도 나지 않고 트리(Tree) 밖에 생각이 나지 않아서 트리로 구현하겠다며 아무말 대잔치를 했는데.. 지금 찾아보니 내가 당시 생각했던 알고리즘과 대충 비슷하긴 하다. 발음도 비슷하다. Trie. 문자열 저장과 prefix 검색에 많이 쓰인다. 나무위키에 따르면 트라이라고 읽어도 되고 트리라고 읽어도 된단다.

특징

  1. 기본적으로 트리(Tree) 기반의 자료구조다.
  2. 자식 노드의 개수는 가변적이다. 즉 배열을 쓰거나?
  3. 트리의 높이는 곧 자료구조 내 저장되어 있는 가장 긴 문자열이 될것이다.
  4. 각 노드는 한개의 문자만을 표현한다.
  5. 한개의 글자씩 노드에 연결되어있기에 N의 길이의 문자열을 검색시 O(N)의 시간복잡도를 가진다. 빠르다.
  6. 대신 메모리를 왕창 먹는다. 공간복잡도가 높다.

대충 그림으로 그려보면 이렇게 생겼다.

trie graph에 대한 이미지 검색결과
http://docs.likejazz.com/wiki/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0/#trie 에서 퍼옴

보통 예제 코드들은 아스키코드, 즉 영어만 처리할 수 있게 만들어 놓은 경우가 많은데 나는 여기에 한글에 대한 처리를 추가했다.

한마디로 한글이라는 단어가 있다면 ㅎㅏㄴㄱㅡㄹ 이렇게 나눠서 처리하는거다. 한글이라는 단어가 Trie 에 포함되어있다면 ㅎ 만 검색해도 한글이 검색결과에 나오게 되는것.

테스트 코드도 넣고 하다보니 블로그에 올리기엔 코드가 꽤 길어져서 따로 git에 올렸다. 링크

IoC 와 DI

라라벨이나 스프링같은 프레임워크를 써본사람들은 한번쯤은 들어본 개념일테지만 내 머리속에서는 두루뭉실하게 윤곽만 그려져 있을뿐, 이게 뭔지 도대체 왜 써야하는건지, DI와 IoC의 관계는 어떻게 되는지 간략하게 정리해봐야겠다.

IoC: Inversion of Control

일명 제어의 역전 원칙이다. 소프트웨어 개발에서 프레임워크를 사용하게 되면 프로그램의 흐름이 프레임워크에 의해서 제어된다. 프레임워크는 프레임워크 사용자가 구현한 코드를 호출한다. 사용자는 프레임워크에 의해 주어진 인터페이스를 구현하고, 해당 구현체를 IoC Container에 등록한다. 그러면 그 구현체를 프레임워크가 필요할때마다 호출함으로써 개발자의 코드가 실행된다.

라라벨은 인터페이스를 Contract(계약) 이라고 부르는 관습이 있는데 인터페이스를 프레임워크 사용자(개발자)와 프레임워크간 계약이라고 본다고 생각하면 어느 정도 수긍이 간다.

Service Locator, DI 등의 방식으로 구현할 수 있다.

그럼 DI는 뭐지?

DI: Dependency Injection 은 클래스가 의존하는 클래스의 인스턴스를 주입받는 것이다. 내가 이해한 바로는 DI는 구체적으로는 IoC 컨테이너를 사용하는 방법 중 하나이며 크게는 클래스간의 결합도를 낮추는 디자인 원칙 중 하나다.

IoC 컨테이너는 여러 인터페이스에 대응하는 구현체를 담고있다고 앞서 설명했는데, 핵심은 사용자가 구현하는 클래스가 의존하는 클래스의 객체를 생성자로 주입받는다는 것이다. 누군가는 Dependency injection is passing an argument 라고 했다. 링크

사실 난 이말에 크게 공감한다. 이름만 멋드러지게 지어놨을뿐 사실 별거 아닌 개념이다. 의존성을 클래스 외부에서 주입하고, 클래스의 생성자에서 요구하는 의존 클래스는 추상화 되어야 좋다. 클래스를 변경하지 않고도 의존하는 추상 클래스의 구현체를 변경하는 것, 그게 사실 DI를 쓰는 이유니까..

마지막

결국 DI는 IoC 컨테이너를 쓰지 않더라도 생성자로 의존하는 클래스를 넣어주기만 하면 대충 DI라고 볼수 있다. (여기에 의존 클래스의 추상화가 더해지면 완벽한 DI) 하지만 스프링 등의 프레임워크는 생성자에 넣어두면 자동으로 의존성을 해결해주기 때문에 DI, IoC의 개념이 혼동될 수 있을것 같다. 나도 헷갈려서 생각을 정리하면서 몇글자 끄적여봤지만, 혹시 잘못 이해한 부분이 있을지 모르겠다.

Jenkins DNSMulticast 로그??

서버의 하드디스크가 꽉차서 확인해보니 젠킨스 로그파일이 200기가 이상을 차지하고 있었다.. 로그를 확인해보니 javax.jmdns 관련된 로그들로 꽉차 있는걸 확인할수 있었다.

...
[DNSQuestion@985724490 type: TYPE_IGNORE index 0, class: CLASS_UNKNOWN index 0, name: ]
...
이런 내용으로 200기가

관련 키워드로 검색을 좀 해보니..

https://issues.jenkins-ci.org/browse/JENKINS-10160

https://issues.jenkins-ci.org/browse/JENKINS-25369

젠킨스에서 사용하고 있는 라이브러리에 뭔가 문제가 있단다. 해결 방법은 간단하다.

일단 우분투 기준으로 /etc/defaults/jenkins 경로의 파일을 수정하자. 파일의 상단에 JAVA_ARGS 환경 변수 부분을 아래와 같이 수정.

JAVA_ARGS="-Djava.awt.headless=true -Dhudson.DNSMultiCast.disabled=true -Dhudson.udp=-1"

그 다음 젠킨스를 재시작하면 끝!

opcache와 JIT..??

PHP 8에서는 JIT 기능이 추가되면서 성능이 한번더 대폭 상향될거라고 한다. 얼마 전 이 얘기를 들었을 때  순간 헷갈렸던게.. 지금껏 PHP의 opcache가 곧 JIT 구현체라고 생각했었기 때문이다.

궁금해서 여기저기 찾아보니 PHP의 opcache, 즉 opcode를 캐시하는 기능은 비유하자면 Java에서 Java 파일을 .class 파일로 컴파일 하는 과정과 같다. 자바는 JVM에 의해 그 옵코드를 최종적으로 해당 머신에 맞는 기계어로 런타임에 컴파일한다. 즉 옵코드는 머신에 종속적이지 않은, 아직 실행될수 없는 코드인거고 일종의 코드를 인터프리팅 하기 쉽게 번역해놓은 수준이라고 이해하면 쉽다.

현재의 PHP에서 (7 버전대) Opcode를 실행하는 것은 결국 Opcode를 인터프리팅 하는것이라고도 할 수 있다. 이게 문자열 파싱해가면서 인터프리팅 하는것보다야 빠르겠지만, 머신 레벨 기계어로 컴파일된 코드에 비할바가 못되는 것이다!

한편으로 의문인 것이.. PHP는 정적타입의 언어가 아니라, 런타임에 변수의 타입이 결정되기에 컴파일 타임에는 변수의 타입을 알 수가 없을텐데 어떻게 기계어로 미리 컴파일 해둘 수 있을지 궁금하다. 곧 나올 PHP 7.4버전에서 타입힌팅이 대폭 강화되는 것으로 알고 있긴한데.. JIT 도입을 위한 초석이었을까? 관련 자료를 찾아보니 이런 문제를 nodejs는 Adaptive JITC라는 기능을 통해, 자주 호출되는 곳만 런타임에 기계어로 컴파일해두는 기능을 도입해서 해결했다고 한다.

아무튼.. 2021년에 릴리즈 예정이라던데.. 궁금하고 기대된다. 번외로, PHP 7.4에서는 opcache preload 기능이 추가되면서 30%정도의 성능향상이 있을것이라고 한다. 언제부턴가 PHP 코어팀이 정말 열일하는 느낌..

gst-rtsp-server 삽질기 1

토이프로젝트로 간단한 오디오 스트리밍 서버를 구현하려고 리서치 해보았는데 MDN에서 GStreamer를 언급한걸 보고 한번 공부해보기로 했다.

GStreamer (이하 gst) 를 이용해 만든 rtsp 스트리밍 서버, gst-rtsp-server를 사용하면 rtsp 스트리밍을 어렵지 않게 구현할 수 있을것 같았다. 오늘 삽질한 과정을 간략히 정리한다. 환경은 우분투 18.04.

먼저, GStreamer 라이브러리와 gst-rtsp-server 라이브러리는 모두 쉽게 패키지 관리자로 설치가 가능했다.

sudo apt-get install --no-install-recommends -y \
    libgstrtspserver-1.0-dev \
    gstreamer1.0-plugins-ugly \
    gstreamer1.0-plugins-base \
    gstreamer1.0-plugins-bad \
    gstreamer1.0-plugins-good \
    gstreamer1.0-x \
    gstreamer1.0-libav \
    gstreamer1.0-tools

CMakeLists.txt에 다음을 추가한다.

include(FindPkgConfig)
find_package(PkgConfig)
pkg_search_module(GSTREAMER REQUIRED gstreamer-1.0)
find_library(LIB_GSTREAMER NAMES ${GSTREAMER_LIBRARIES} HINTS ${GSTREAMER_LIBRARY_DIRS})
FIND_LIBRARY(GST_RTSP_SERVER_LIBRARY_DIRS NAMES gstrtspserver-1.0 PATHS "/usr/lib/x86_64-linux-gnu/gstreamer-1.0/")
FIND_PATH(GST_RTSP_SERVER_INCLUDE_DIRS gst/rtsp-server/rtsp-server.h PATHS "/usr/include/gstreamer-1.0")
pkg_search_module(GSTREAMER_APP REQUIRED gstreamer-app-1.0)
include_directories(
    "${GST_INCLUDE_DIRS}"
    "${GST_RTSP_SERVER_INCLUDE_DIRS}"
    "${GSTREAMER_APP_INCLUDE_DIRS}"
)

...

target_link_libraries(your_project_name
    "${GSTREAMER_LIBRARIES}"
    "${GSTREAMER_APP_LIBRARY_DIRS}"
    "${GST_RTSP_SERVER_LIBRARY_DIRS}"
)

메인 함수에 다음과 같이 써서 빌드하고 실행해본다.

#include <iostream>
#include <gst/gst.h>
#include <gst/rtsp-server/rtsp-server.h>

int main(int argc, char *argv[]) {
    guint major, minor, micro, nano;
    gchar *nano_str;

    gst_init(&argc, &argv);

    gst_version(&major, &minor, &micro, &nano);

    if (nano == 1)
        nano_str = (gchar*)"(CVS)";
    else if (nano == 2)
        nano_str = (gchar*)"(Prerelease)";
    else
        nano_str = (gchar*)"";

    printf ("This program is linked against GStreamer %d.%d.%d %s\n",
            major, minor, micro, nano_str);

    auto *server = gst_rtsp_server_new ();
    auto *loop = g_main_loop_new(NULL, FALSE);

    /* attach the server to the default maincontext */
    gst_rtsp_server_attach(server, NULL);

    std::cout << "Server is listening on " << gst_rtsp_server_get_address(server) << ":" << gst_rtsp_server_get_bound_port(server) << std::endl;

    /* start serving */
    g_main_loop_run(loop);

    return 0;
}

아래와 같이 나오면 성공!

This program is linked against GStreamer 1.14.1 
Server is listening on 0.0.0.0:8554

글로 적으니까 쓸게 별로 없다. 쉬운건데 CMake를 처음써봐서 한참 헤맸던것 같다. 다음번엔 mp3 파일을 스트리밍 한번 해보고, 삽질기 2탄을 올려봐야징

docker 호스트머신에 연결하기

docker 사용시 호스트 네트워크에 접근해야 하는 경우가 있다. 예를들면 도커 컨테이너 안에서 로컬(Host machine)에 설치된 DB에 접속하려는 경우, localhost로는 접근이 안되고 실제 아이피를 입력해야한다. 아이피는 환경에 따라 매번 바뀌기 때문에 환경설정이 자꾸 바뀌면 골치아픈데,  도커 18.03버전부터는 host.docker.internal 이라는 dns name으로 접속이 가능하다.