Making Myself Obsolete

Matthias Endler

나를 쓸모없게 만들기

스테고사우루스는 1억 5천만 년 전에는 더 멋진 시절을 보냈다.
스테고사우루스는 1억 5천만 년 전에는 더 멋진 시절을 보냈다.
출처: 고생물학자들은 한때 스테고사우루스의 엉덩이에 뇌가 있다고 생각했다.

2015년 12월, 저는 trivago의 CI 과정에 통합할 정적 분석 도구를 찾고 있었습니다. 전형적인 프로그래밍 실수를 자동으로 탐지하려는 목적이었습니다. 그리 특별한 일도 아니었고, 목적에 맞는 유용한 도구도 이미 많이 나와 있었습니다.

그래서 도구 목록을 찾아봤습니다…

놀랍게도 제가 찾은 유일한 목록은 위키백과에 있었고 — 게다가 오래된 정보였습니다. 최신 정적 분석 도구 대부분이 올라와 있는 Github에는 그런 프로젝트가 없었습니다.

깊게 고민하지 않고 에디터를 열어 초기 조사에서 찾은 도구 몇 개를 적어 내려갔습니다. 그리고 그 목록을 Github에 올렸습니다.

프로젝트 이름은 Awesome Static Analysis라고 지었습니다.

2년이 흘렀고 목록은 꽤 많이 커졌습니다. 지금까지 75명의 기여자와 277개의 포크, 2,000개가 넘는 스타를 받았습니다. (많은 성원에 감사드립니다!) (2018년 5월 업데이트: 기여자 91명, 포크 363개, 스타 3,000개 이상) (2025년 10월 업데이트: 기여자 316명, 포크 1,400개, 스타 14,000개 이상)

매주 약 1,000명의 순 방문자가 이 목록을 찾아옵니다. 절대 많은 수는 아니지만, 많은 사람들에게 필수적인 정보원이 되었기 때문에 계속 최신 상태로 유지해야 한다는 책임감을 느낍니다.

현재 목록에는 약 300개의 정적 분석 도구가 올라와 있습니다. Ada부터 TypeScript까지 모든 언어가 망라되어 있습니다. 특히 동기가 되는 점은 이제 도구 제작자들이 직접 풀 리퀘스트를 만들어 자신의 도구를 추가한다는 사실입니다!

하지만 한 가지 문제가 있었습니다. 제가 다른 일로 바쁜 사이 풀 리퀘스트 목록이 점점 길어지고 있었던 것입니다.

awesome-static-analysis의 Github 풀 리퀘스트 목록
awesome-static-analysis의 Github 풀 리퀘스트 목록

기여자 추가하기

저는 꾸준히 기여하는 분들을 팀원으로 영입하려고 노력합니다. 친구이자 동료인 Andy GrunwaldOuroboros Chrysopoeia는 모두 소중한 협력자입니다. 두 분은 시간이 날 때마다 새로운 PR을 검토하는 일을 도와줍니다.

하지만 솔직히 말하면 풀 리퀘스트를 확인하는 일은 지루한 수작업입니다. 새로 추가되는 도구마다 확인해야 할 항목은 다음과 같이 요약할 수 있습니다.

  • 서식 규칙을 준수했는지
  • 프로젝트 URL에 접속할 수 있는지
  • 라이선스 표기가 정확한지
  • 각 섹션의 도구들이 알파벳순으로 정렬되어 있는지
  • 설명이 너무 길지 않은지

이 체크리스트를 어떻게 해야 할지는 분명합니다. 바로 자동화하는 것입니다!

린터를 검사하는 린터

그러니 분석 도구 목록을 검사하는 분석 도구를 만들면 어떨까요! 꽤 메타적으로 들리지만 사실 아주 단순한 일입니다.

풀 리퀘스트가 올라올 때마다 봇을 실행해 위 규칙들을 검사하고 결과를 알려주도록 했습니다.

첫 단계는 CI 서버 구축에 관한 Github 문서를 읽는 것이었습니다.

재미 삼아 봇을 Rust로 만들어 보고 싶었습니다. Rust용으로 가장 인기 있는 Github 클라이언트는 github-rs(현재는 지원 중단됨)와 hubcaps였습니다. 둘 다 꽤 괜찮아 보였지만, 그때 afterparty라는 “Github 웹훅 서버”를 찾게 되었습니다.

예제 코드는 정말 멋져 보였습니다.

#[macro_use]
extern crate log;
extern crate env_logger;
extern crate afterparty;
extern crate hyper;

use afterparty::{Delivery, Hub};

use hyper::Server;

pub fn main() {
    env_logger::init().unwrap();
    let addr = format!("0.0.0.0:{}", 4567);
    let mut hub = Hub::new();
    hub.handle("pull_request", |delivery: &Delivery| {
        match delivery.payload {
            Event::PullRequest { ref action, ref sender, .. } => {
                // TODO: My code here!
                println!("sender {} action {}", sender.login, action)
            }
            _ => (),
        }
    });
    let srvc = Server::http(&addr[..])
                   .unwrap()
                   .handle(hub);
    println!("listening on {}", addr);
    srvc.unwrap();
}

덕분에 실제 분석 코드에만 집중할 수 있었는데, 사실 그 코드는 읽어봐도 그다지 재미있지는 않습니다. 위에서 언급한 항목들을 기계적으로 검사할 뿐이고 어떤 언어로든 작성할 수 있는 코드입니다. 직접 살펴보고 싶거나 기여하고 싶다면 저장소를 확인해 보십시오.

Github와 대화하기

분석 코드를 완성한 뒤에는 로컬에서 실행되며 들어오는 풀 리퀘스트를 기다리는 봇이 생겼습니다.

하지만 어떻게 Github와 통신할 수 있을까요? Status API를 사용해 POST 요청을 /repos/mre/awesome-static-analysis/statuses/:sha로 보내야 한다는 것을 알게 되었습니다.
(:sha는 풀 리퀘스트의 HEAD를 가리키는 커밋 ID입니다):

{
  "state": "success",
  "description": "The build succeeded!"
}

기존 Rust용 Github 클라이언트 중 하나를 사용할 수도 있었지만, 풀 리퀘스트 상태를 업데이트하는 간단한 함수를 직접 작성하기로 했습니다.

fn set_status(status: Status, desc: String, repo: &str, sha: &str) -> Result<reqwest::Response> {
    let token = env::var("GITHUB_TOKEN")?;
    let client = reqwest::Client::new();
    let mut params = HashMap::new();
    params.insert("state", format!("{}", status));
    params.insert("description", desc);
    println!("Sending status: {:#?}", params);

    let status_url = format!("https://api.github.com/repos/{}/statuses/{}", repo, sha);
    println!("Status url: {}", status_url);
    Ok(client
        .request(
            reqwest::Method::Post,
            &format!(
                "{}?access_token={}",
                status_url,
                token,
            ),
        )
        .json(&params)
        .send()?)
}

환경 변수에서 Github 토큰을 가져온 뒤 reqwest 라이브러리를 이용해 JSON 페이로드를 POST 요청으로 보내는 것을 볼 수 있습니다.

그런데 결국 이 부분이 문제가 되었습니다. afterparty는 hyper 0.9 버전을 사용하고 있었는데, reqwest는 0.11을 사용하고 있었습니다. 안타깝게도 두 버전은 서로 다른 빌드의 openssl-sys 바인딩에 의존하고 있었습니다. 이는 잘 알려진 문제이며 해결하는 유일한 방법은 충돌을 해소하는 것이었습니다.

한동안 막혀 있었지만, afterparty를 hyper 0.10으로 업그레이드하는 풀 리퀘스트가 이미 올라와 있다는 것을 알게 되었습니다.

그래서 Cargo.toml에서 afterparty 버전을 해당 풀 리퀘스트의 버전으로 고정했습니다.

[dependencies]
afterparty = { git = "https://github.com/ms705/afterparty" }

이렇게 빌드 문제를 해결하고 마침내 다음 단계로 넘어갈 수 있었습니다.

배포

봇을 호스팅할 곳이 필요했습니다.

비영리 오픈소스 프로젝트였기 때문에 가능하면 무료였으면 했습니다. 또 바이너리를 실행할 수 있는 곳이어야 했습니다.

한동안 zeit라는 제품을 지켜보고 있었습니다. 이 서비스는 now라는 직관적인 커맨드라인 인터페이스를 이용해 어떤 Docker 컨테이너든 실행할 수 있게 해줍니다.

사이트에서 데모를 처음 봤을 때 바로 마음에 들었고, 한 번 써보고 싶었습니다.

그래서 프로젝트에 멀티 스테이지 Dockerfile을 추가했습니다.

FROM rust as builder
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN cargo build --release

FROM debian:stretch
RUN apt update \
    && apt install -y libssl1.1 ca-certificates \
    && apt clean -y \
    && apt autoclean -y \
    && apt autoremove -y
COPY --from=builder target/release/check .
EXPOSE 4567
ENTRYPOINT ["./check"]
CMD ["--help"]

첫 번째 파트에서는 정적 바이너리를 빌드하고, 두 번째 파트에서는 컨테이너가 시작될 때 그 바이너리를 실행하도록 했습니다. 그런데 잘 되지 않았습니다. zeit가 아직 멀티 스테이지 빌드를 지원하지 않았기 때문입니다.

해결책은 Dockerfile을 두 개로 나누고 Makefile로 두 파일을 연결하는 것이었습니다. Makefile은 정말 강력합니다. 아시지요?

이렇게 배포에 필요한 모든 준비를 마쳤습니다.

# Build Rust binary for Linux
docker run --rm -v $(CURDIR):/usr/src/ci -w /usr/src/ci rust cargo build --release

# Deploy Docker images built from the local Dockerfile
now deploy --force --public -e GITHUB_TOKEN=${GITHUB_TOKEN}

# Set domain name of new build to `check.now.sh`
# (The deployment URL was copied to the clipboard and is retrieved with pbpaste on macOS)
now alias `pbpaste` check.now.sh

now를 이용해 배포했을 때의 출력은 다음과 같습니다.

> Deploying ~/Code/private/awesome-static-analysis-ci/deploy
> Ready! https://deploy-sjbiykfvtx.now.sh (copied to clipboard) [2s]
> Initializing…
> Initializing…
> Building
> ▲ docker build
Sending build context to Docker daemon 2.048 kBkB
> Step 1 : FROM mre0/ci:latest
> latest: Pulling from mre0/ci
> ...
> Digest: sha256:5ad07c12184755b84ca1b587e91b97c30f7d547e76628645a2c23dc1d9d3fd4b
> Status: Downloaded newer image for mre0/ci:latest
>  ---> 8ee1b20de28b
> Successfully built 8ee1b20de28b
> ▲ Storing image
> ▲ Deploying image
> ▲ Container started
> listening on 0.0.0.0:4567
> Deployment complete!

마지막 단계는 awesome-static-analysis 프로젝트 설정에서 check.now.sh를 웹훅으로 추가하는 것이었습니다.

이제 새로운 풀 리퀘스트가 들어올 때마다 저 작은 봇이 동작하는 모습을 볼 수 있습니다!

봇이 검사한 성공적인 풀 리퀘스트

결과와 향후 계획

도구 선택에 매우 만족합니다. afterparty 덕분에 많은 수작업을 덜 수 있었고, zeit 덕분에 배포가 정말 쉬워졌습니다.
마치 스테로이드를 맞은 Amazon Lambda 같은 느낌입니다.

코드와 봇의 커밋 내역을 보면 모든 것이 제대로 동작하기까지 제가 저지른 사소한 실수들을 볼 수 있습니다. 알고 보니 사람이 읽는 텍스트를 파싱하는 일은 꽤 지루한 작업이었습니다.
그래서 분석 도구 목록을 YAML 같은 구조화된 형식으로 바꾸는 것을 고민하고 있습니다. 이렇게 하면 파싱이 훨씬 간단해질 뿐만 아니라 다른 프로젝트에서도 활용할 수 있는 기계가 읽을 수 있는 도구 목록을 갖게 된다는 장점도 있습니다.

2018년 5월 업데이트

비엔나에서 열린 WeAreDevelopers 컨퍼런스에 참석하는 동안(추천합니다) CI 파이프라인을 zeit.co에서 Travis CI로 옮겼습니다. 린팅 코드를 프로젝트 바로 옆에 두고 싶었기 때문인데, 덕분에 많은 것이 훨씬 간단해졌습니다. 무엇보다 이제 웹 요청 처리 코드를 더 이상 직접 작성할 필요가 없습니다. travis가 알아서 처리해 주기 때문입니다. 원하신다면 이전 버전과 버전을 비교해 보십시오.

2025년 10월 업데이트

그 이후로 그 프로젝트의 거의 모든 것이 바뀌었습니다. 이제는 Github Actions로 CI 검사를 실행하고, README.md는 각 도구의 YAML 파일로부터 완전히 자동 생성됩니다. 추가 리소스 목록(튜토리얼, 영상 등)이나 도구가 제공하는 유료 플랜 목록 같은 특별한 메타데이터 필드도 지원합니다. 이제는 700개가 넘는 도구를 나열하고 투표할 수 있는 멋진 웹사이트도 생겼습니다. 웹사이트는 Next.js로 만들었고 검색에는 Algolia를 사용합니다. 호스팅 등의 비용을 도와주는 후원자들도 많이 생겼습니다. 애정과 시간을 쏟으면 프로젝트가 어떻게 성장하는지 보는 것은 정말 놀라운 일입니다! 이 글이 여러분이 직접 오픈소스 프로젝트를 시작하는 데 영감이 되기를 바랍니다! 그리 어렵지 않고, 정말 보람 있는 일입니다.

원문은 Matthias Endler님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.