사람들이 오픈소스 기여에 대해서 어렵게 느끼거나 부담을 느끼는 경우가 많다.
앞으로 내가 오픈소스들에 기여한 점들에 대해서 하나하나 적어보고자 한다.
오픈소스를 기여할 때 이 사람은 어떤 생각 흐름을 가지는지, 어떤 과정들을 밟는지. 글을 읽고 조금이나마 부담을 더는데 도움이 되었으면 한다.
tus는 RFC 7230와 RFC 7540에 소개된 resumable upload protocol 메카니즘을 제공하는 프로토콜이다.
네트워크 위에서 대용량 파일을 전달하는 도중에 중단이 발생하는 경우들을 신경쓰고 싶지 않다면, 이 프로토콜을 사용하는게 유용할 수 있다. 서버 구현체와 Client 구현체들을 벤더에서 직접 제공하고 있고, 3rd-party 라이브러리들 또한 공식 문서에서 찾아볼 수 있다.
나는 사내에서 Object Storage를 지원하기 위해 tus/tusd를 사용하고 있다.
요즘은 S3 프로토콜과도 호환되는 Minio를 많이 사용하는 것 같다. 하지만 K8s Cluster 환경을 전제로 하고 있고, 고객사의 EC2 인스턴스에 제품을 배포하는 특성 등을 고려하다 보니, tusd 구현체를 사용하게 되었다. 또한 단순 Object Storage를 벗어나는 요구사항들을 커스터마이징 하기에도 적절했다.
위 구조는 다음과 같은 생각을 가지고 만들었다.
이 때 사용한 tusd의 Local Disk 구현체에서 문제가 발생했고, tusd에 기여하는 계기가 되었다.
나는 Local Disk 구현체를 사용하기 때문에, Linux FileSystem 의 Directory 구조에 의존한다. 하지만, Linux FileSystem 의 Directory 구조에는 하나의 디렉토리에 무한한 파일을 저장할 수 없다는 문제가 있다.
https://stackoverflow.com/questions/466521/how-many-files-can-i-put-in-a-directory
또한 글에서. 현대의 파일시스템은 IntMax 만큼의 파일은 수용할 수 있긴 하지만, 몇 개 이상의 파일이 하나의 디렉토리 안에 같이 있으면 성능 문제가 발생할 수 있다는 내용이있다. 내부적으로 AWS EFS 기준으로 성능테스트를 수행한 결과 약 3000개 이상의 파일이 한 디렉토리 안에 있으면 I/O가 불규칙적으로 튀는것을 확인할 수 있었다. 따라서 보수적으로 하나의 디렉토리 안에 1000개 이상의 파일이 공존하지 않도록 설계가 필요했다.
내부적으로 FS에서 직접적으로 달성할 수 있는 요구사항(년/월 별로 파일을 관리하고 싶어요)을 만족시키는 구조로 폴더를 설계했고, 다음과 같은 구조가 되었다.
/{yyyyMM}/{dd}/{HHmm}/{ss}/{fi}/{le}/{na}/{me}/{filename}
1초에 1000개 이상의 파일이 저장되는 경우 문제가 될 수 있기에, {fi}/{le}/{na}/{me} 부분이 있다. /a-z0-9/ 조합이 1369이기 때문에 타협할만한 수준으로 판단했다.
이와 같은 hierarchy 구조로 tusd를 사용하고 보니, 파일이 존재하지 않을 때 404 응답이 아닌 500 응답이 내려오고 있는 현상을 확인했다. 소스코드를 확인해 본 결과 아래와 같은 부분에서 발생하고 있었고, 외부에서 커스터마이징 하기에는 너무 깊이 있는 부분이라서 오픈소스에 컨트리뷰션 하기로 결심했다.
func (handler *UnroutedHandler) GetFile(http.ResponseWriter, *http.Request) {
// ...
if handler.composer.UsesLocker {
lock, err := handler.lockUpload(c, id) // <-- (1)
if err != nil {
handler.sendError(c, err)
return
}
defer lock.Unlock()
}
// ...
}
func (handler *UnroutedHandler) lockUpload(c *httpContext, id string) (Lock, error) {
// ...
if err := lock.Lock(ctx, releaseLock); err != nil { // <-- (2)
return nil, err // <-- (3) *fs.PathError (ENOENT)
}
// ...
}
GetFile 에서 ENOENT 에러가 적절히 처리되지 못했고, HTTP Server 입장에서 핸들링 되지 않은 에러는 500 에러인 것이 당연했다. 때문에 Handler는 HTTP Server 에게 적절히 핸들링 된 에러로 전파해야했다.
그러나 (2)에서 볼 수 있는 lock 은 추상화된 Lock 이었고, Local Disk 인 경우 해당 Lock의 구현체는 lockfile 라이브러리에 의존하고 있었다.
(3)에서 error가 ENOENT 인 경우 핸들링 하면 된다고 생각할 수 있지만, lock 이 추상화되어있기 때문에, filesystem 의 에러를 갑자기 핸들링 하는건 어색하다. 때문에 lockfile 라이브러리의 내부로도 들어가보지만, lockfile 에서 에러를 핸들링 하더라도 미리 정의된 Error를 던지고 외부에서 검사하는 형태가 되어야만 하는데, 이 또한 결국 (3)에서 특수한 경우의 에러를 핸들링 하는 것이기 때문에 어색해 보인다.
나는 더 이상의 시간을 쏟더라도 이 부분을 깔끔하게 해결하기 어렵겠다는 생각을 했다. 내가 알지 못하는 다른 히스토리나 해결방법이 있을 수 있고, 결국 소스코드 전반을 가장 잘 아는건 메인테이너기 때문에 결국 이슈를 제보하는 방향을 택했다. 위에서 고민한 내용들을 적절히 잘 설명하는데 힘을 쏟았다.
https://github.com/tus/tusd/issues/1148
링크 안에서 확인할 수 있겠지만, 이 문제는 메인테이너도 결국 주석에 "filelocker에 대한 특수한 처리" 라고 남기고 ENOENT 에러를 핸들링했다.
그리고 나에게 "핸들링 하기에 적절하지 않다는 것에 대해 같은 의견이며, 향후 이 구조를 더 고민해보겠다" 고 답변을 남겼다.
급하게 글을 마치는 느낌이 없진 않지만, 이렇게 나는 tus/tusd 에 기여했다.
Issue 제보하는게 무슨 기여냐고 할 수 있겠지만, 오픈소스는 사용해주는 사람이 많을 수록 발전하고 성장한다.
버그를 많이 제보할 수 록 더 버그가 없는 소프트웨어가 되고, 다양한 사용자들이 다양한 측면에서 QA를 수행해주는 것과 마찬가지이기 때문에 버그 제보는 늘 도움이 된다.
하지만, 같은 이슈가 있는지 검색해보지 않거나, 본인에게만 필요할 수 있는 요구사항만을 남기는 Spam성 Issue 제보는 지양해야한다.
나는 이 이슈를 재현하고 코드를 고민하는데에 약 3시간의 시간을 쏟았다. 메인테이너에게는 20분이면 해결될 수 있겠지만, 나는 메인테이너의 시간을 아껴준 것이다. 또한 내가 고민한 내용들을 공유하고, 공감받거나 토론하는게 오픈소스의 또 다른 재미이다. 오픈소스의 다양한 코드들을 보며 설계 철학을 배우는 등의 행위도 개발자로써 발전할 수 있는 시간이 된다는데에 확실하게 얘기할 수 있다. 책에서 얻을 수 있는 지식도 있지만, 분명 훨씬 더 재미있고 값질 것이다.