altair의 프로젝트 일기
ImageCompressor2 개발기 본문
가끔 아카이빙 했던 사진들을 둘러보곤 한다. 그리운 옛날 생각도 나고 그때 기분도 다시 느낄 수 있었다. 그러다 어느 날 모든 사진들의 용량을 살펴봤다. 80기가가 넘는 용량이었다. 평소라면 별생각 없이 넘겼겠지만, 그때는 뭔가 과하다는 생각이 들었다. 모든 사진 스캔본이 png 파일로 저장되어 있는 것이 아닌가? 내가 배운 것이 맞다면 png 포맷은 무손실 압축 포맷이라 화질이 좋지만 비교적 용량이 크다. 실제로 일기 스캔본을 확대해보니 흑연이 종이에 뭍은 모습까지 보였다.
어떤 사진은 해상도보다 내용이 중요한 사진들이 있다. '얼마나 많은 머리카락이 보이느냐'보다 '그 사진에 누가 어떤 모습으로 나와있나'가 더 중요한 사진들 말이다. 모임에서 친구들과 찍은 사진이나 일기 스캔본은 어느 정도 이상의 화질은 크게 의미 없다. 옛날 필름 카메라로 찍은 사진들을 스캔한 것은 원본 자체의 해상도가 매우 낮기 때문에 스캔본의 해상도가 아무리 높다고 해도 한계가 있다. '과잉 화질'인 것이다.
이런 사진 파일들을 jpg로 변환하고 화질을 낮춰 보관하면 매우 편리할 것 같다는 생각을 했었다. 전체 용량을 줄일 뿐만 아니라 이를 통해 파일들을 가볍게 만들어 여러 곳에 편하게 보관하고 쉽게 찾아볼 수 있도록 하고 싶었다.
그래서 1년 전쯤 파이썬으로 커맨드 라인 프로그램인 ImageCompressor를 만들었었다. 이걸 사용해 80기가에 달하던 사진 파일들을 4기가로 압축할 수 있었고 DVD에 구워 보관할 수도 있었다. 더욱이 그동안 찍은 휴대폰의 사진이나 기타 사진 파일들도 이 프로그램을 사용해 작은 크기로 보관할 수 있었다.
하지만 어느 날 이 프로그램이 webp를 제대로 jpg로 변환하지 못한다는 것을 발견했다. 물론 크기로만 따지면 webp는 굳이 변환할 필요 없는 포맷이긴 하나, 윈도우 사진 뷰어가 webp를 열지 못하는 것과 같이 아직 모든 기기에서 충분히 지원하지 않는다는 점을 생각하면 이 또한 jpg로 변환해 보관하고 싶었다.
마침 새로운 언어로 Rust를 배우고 있었기 때문에 ImageCompressor의 재구현과 발전을 새 러스트 토이 프로젝트로 삼았다. 게다가 파이썬으로 GUI를 구성해본 경험을 돌이켜 생각해보면, 굉장히 느리고 버벅거리는 파이썬에 비해(이 이유 때문에 커맨드 라인 프로그램으로 만들 수밖에 없었다) 가볍고 빠른 러스트는 더 반응성이 좋은 GUI를 만들 수 있게 해 줄 것 같았다(실제로도 그랬다!).
프로젝트 목표는 다음과 같다.
- 이전 버전의 프로그램이 지원한 이미지 포맷들을 모두 지원할 것.
- webp를 비롯한 새로운 이미지 포맷들을 지원할 것.
- 결과적으로 확장성이 매우 떨어진 이전 버전과 다르게 충분히 확장성을 챙길 것.
- 빠른 언어인 만큼 GUI 단을 만들어 커맨드 라인에 익숙하지 않은 사용자들도 사용할 수 있도록 할 것.
- 멀티스레드를 사용할 것.
- 7z를 사용해 압축파일 생성을 지원할 것.
파이썬으로 구현한 이전 버전은 불행히 pathlib 패키지를 제대로 사용하지 않았다. 파일 경로를 깊게 다루어 본 적이 없기도 했고 이런 간단한(결과적으로는 아니었지만) 일을 하는 프로그램에 귀찮은 노력을 기울이고 싶지 않았다. 경로를 다루는 코드가 길고 복잡해졌고 애매한 이름들이 사용되었다. 확장하기 어려운 코드가 되고 말았다.
저번 자바 프로젝트에서 경험했던 단위 테스트를 사용했다. 코드를 작은 부분으로 나누고 테스트하였다. 아직은 많이 미숙하지만 저번보다는 더 나은 코드를 작성할 수 있었다.
아래는 압축 후 사진의 모습이다. 보다시피 충분히 글자를 알아볼 수 있다. 파일의 크기는 47MB에서 300KB로 줄어들었다. 화질을 조금 희생해서 용량을 거의 1/100로 압축할 수 있었다.
결론적으로 80기가에 달하던 아카이브 폴더를 2.8기가로 변환할 수 있었다. 파이썬으로 만든 이전 프로그램이 4.4기가로 압축했던 것에 비하면, 리사이징 비율과 퀄리티 값을 그대로 사용했음에도 불구하고 더 효율적으로 압축한 것이다. 게다가 더 빠르고 편한 GUI를 구현하였으며 프로그램 작동의 중요한 역할을 하는 image_compressor 패키지를 cargo.io에 업로드할 수 있었다.
이제 프로젝트를 진행하면서 경험한 러스트의 장단점에 대해 말해보고자 한다.
장점
빠르다.
정확한 벤치마크 상에서는 C/C++과 비슷한 속도를 보인다. 실제로 경험한 프로그램의 속도도 충분히 빨랐다. 특히 프로그램이 GUI를 돌릴 때, 그리고 이미지를 압축할 때 이 장점이 부각되었다. 파이썬으로 만들었던 GUI보다 훨씬 빠른 속도였다.
안전하다.
로우 레벨 언어, 특히 C/C++를 사용할 때 발생하는 대부분의 오류는 메모리 관련 오류일 것이다. 물론 스마트 포인터를 사용하면 많이 나아지기는 하지만, 멀티스레드를 사용하거나 방대한 프로젝트를 진행한다면 언젠가 터질 시한폭탄을 안고 코딩하는 기분이다. 하지만 러스트에서는 비록 강제적이고 엄격하긴 하지만 소유권과 라이프타임이 이런 문제를 걱정할 필요가 없게 해 준다.
데이터는 언제나 변수 하나에만 저장할 수 있으며 스코프를 벗어나면 해제된다. 함수 인자의 경우에도 마찬가지다. 유일한 예외는 참조자인데 참조자의 수명이 원본 데이터보다 작거나 같다는 확실한 보장이 있어야 한다. 이런 점들이 러스트를 안전한 언어로 만들어준다. 멀티스레드 프로그램을 훨씬 안전하게 작성할 수 있다.
또한 null이 언어적으로 존재하지 않는다. 대신 Option으로 Some(T)인지 None인지 여부를 저장할 수 있으며 두 경우의 분기를 모두 명시하지 않으면 컴파일이 되지 않는다. 예외 역시 비슷한 방식으로 작동하므로 놓치는 분기나 NullPointerError가 일어나지 않는다.
의존성 관리가 쉽다.
C/C++와 달리 파이썬은 pip를 통해 여러 패키지를 손쉽게 받아 쓸 수 있다. venv나 conda를 사용하면 패키지들을 더 쉽게 관리할 수 있다. 러스트는 파이썬의 pip처럼 cargo라는 빌드 시스템 및 패키지 관리자를 갖고 있다. pip보다 훨씬 많은 기능을 갖고 있는데, 프로젝트의 의존성을 관리하고 패키지를 다운로드할 뿐만 아니라 프로젝트를 테스트, 실행, 빌드, 릴리즈할 수 있다. 또한 주석을 문서화하거나 자신이 만든 패키지를 cargo.io에 업로드할 수도 있다. 코드를 관리할 때 필요한 거의 모든 일을 cargo로 간편하게 실행할 수 있다.
그리고 무엇보다 프로젝트가 사용하는 외부 패키지의 버전을 관리해 준다. 처음 의존성을 추가할 때는 최신 버전의 패키지만 추가할 수 있지만, 이후에 새 버전이 나왔어도 쓰던 버전을 계속 쓸 수 있게 해준다.
Cargo.io
파이썬이 강력하다고 하는 이유 중에 하나는 바로 수많은 외부 모듈들일 것이다. 간단한 파일 압축 모듈부터 머신러닝 라이브러리까지 엄청 방대한 모듈들이 파이썬을 매력적인 언어로 만든다. 그 정도까지는 아니지만 러스트도 Cargo.io라는 패키지 저장소를 가지고 있다. 아직 만들어진지 얼마 되지 않은 언어라 파이썬만큼 많은 패키지가 있지는 않지만 그래도 꽤 많은 패키지들이 있다. 나 또한 이 프로젝트를 진행할 때 이미지 처리 패키지와 GUI 패키지를 이곳에서 찾아 사용할 수 있었다. 라이브러리를 한 곳에서 관리해주는 건 항상 정말 매력적이다.
문서화가 쉽다.
Java의 javadoc이나 C/C++의 doxygen이 해주던 주석의 문서화를 cargo가 쉽게 해 준다. 게다가 따로 웹서버에 올리지 않으면 문서를 대중에 공개하기 어려웠던 이들과 달리, cargo.io에 업로드만 한다면 그 문서 페이지를 자동으로 생성해준다. 개발자가 할 일은 단지 문서화 주석을 충실히 쓰고 업로드하는 것뿐이다. 문서 페이지는 cargo와 cargo.io가 알아서 만들어준다. 러스트 패키지들은 나름 문서화가 잘 되어있는 편이다. 쉽기 때문이다.
패러다임을 취사 선택했다.
개인적으로 가장 놀라웠던 점은 객체지향을 완전히 구현하지 않은 것이다. Java나 C++ 이 갖는 가장 대표적인 특징인 상속을 러스트는 갖고 있지 않다. 상속을 사용하는 가장 대표적인 이유는 코드 재사용과 다형성 때문이다. 부모의 기능을 고스란히 사용하거나, 같은 부모로부터 상속받은 객체들을 부모의 형으로써 사용할 수 있는 것이다. 러스트는 트레잇으로 이를 구현한다. 트레잇의 기본 구현을 그대로 사용하는 것을 통해 코드 재사용을 할 수 있으며 같은 트레잇을 갖는 객체들을 트레잇 객체로서 한데 모아 다형성을 사용할 수 있다. 상속이 아닌 트레잇을 사용했기에 다중 상속이나 부모, 조부모까지 생각해야 하던 일이 벌어지지 않는다.
러스트는 위험을 줄이고 유연성을 늘리기 위해 이러한 선택을 했다. 상속보다 안전한 방식으로 훨씬 유연한 프로그램을 작성할 수 있었다. 최신 언어답게 클로저와 이터레이터를 지원하는 것은 덤이다.
단점
까다롭다.
다른 언어들, 특히 파이썬과 같은 동적 타이핑 언어에 비해서 훨씬 깐깐한 문법을 요구한다. 게다가 C/C++와 같은 평범한 정적 타이핑 언어보다 더 까다로운 컴파일 과정을 거친다. 개인적으로 동적 타이핑보다는 정적 타이핑이 훨씬 좋지만, 소유권이나 라이프타임에서 매번 컴파일 에러를 내뱉는 걸 보면 답답하기까지 한다. 분명 그냥 데이터들을 갖고 실행하면 될 함수나 스레드가 소유권이 이미 없어졌다는 둥, 라이프타임이 정의되지 않았다는 둥 빨간 줄이 그어지면 막막할 때가 있다.
그래도 이런 까다로운 컴파일 체크 때문에 일단 실행된다면 웬만한 오류는 없다고 볼 수 있다.
러스트만을 위한 IDE가 없다.
언어마다 그 언어를 위한 IDE가 있다. Java는 Intellij, C/C++는 Visual Studio나 Clion, Python은 Pycharm, Swift는 Xcode가 가장 주된 IDE라고 할 수 있겠다. 물론 사용자가 그렇게 많지 않아서 그럴 테지만, 러스트는 러스트만을 위한 IDE가 없다. VSC에 확장을 설치해 사용하던가, Intellij에 러스트 플러그인을 설치해 사용하는 수밖에 없다. 모두 사용해본 입장에서 인텔리제이는 너무 무겁고 느렸다. Java로 작성할 때보다 훨씬 느린 코드 분석 속도를 보여주었다. VSC는 빠르고 가벼웠지만 인텔리제이에 익숙해진 입장에서는 많은 편의 기능들을 사용하지 못한다는 게 크게 느껴졌다.
어렵다.
훨씬 까다로운 규칙을 갖고 있기에 진입장벽이 높은 편이다. 특히 첫 언어로 배우기에는 너무 어렵다. 다른 언어들을 먼저 충분히 알고서야 비로소 쉽게 이해되는 것들이 많았다. C/C++, Java에서 원래 있었던 규칙에 더해져 더 많은 제약이 가해진 느낌이다.
외부 패키지들이 많지 않다.
단점이라기보다 신생 언어의 한계일 텐데, 아직은 파이썬처럼 다양한 라이브러리들이 있지는 않다. 웬만한 패키지는 있지만 파이썬에 비하면...
결론
이런저런 어색함과 까다로움에도 불구하고 멀티스레드 프로그램을 이렇게 안전하게 작성할 수 있다는 점, 조금 빡빡할지라도 어디서 언제 터질지 모르는 버그들을 안고 가지 않아도 된다는 게 굉장히 매력적이었다. 왜 그동안 개발자가 가장 사랑하는 언어에 계속 뽑혔는지 알만했다. 나도 앞으로 내가 쓸 프로그램을 만들어야 할 때는 웬만하면 러스트를 사용하고 싶을 정도니까.
참고
ImageCompressor Repository (Python)
GitHub - altair823/ImageCompressor
Contribute to altair823/ImageCompressor development by creating an account on GitHub.
github.com
ImageCompressor2 Repository (Rust)
GitHub - altair823/ImageCompressor2
Contribute to altair823/ImageCompressor2 development by creating an account on GitHub.
github.com
'IT > 러스트' 카테고리의 다른 글
image_compressor 라이브러리 개선하기(2) (0) | 2024.03.04 |
---|---|
image_compressor 라이브러리 개선하기(2) - 테스트 개선하기 (0) | 2023.07.11 |
image_compressor 라이브러리 개선하기(1) (0) | 2023.07.03 |
러스트로 tar 직접 구현하기 (2) (0) | 2022.12.27 |
러스트로 tar 직접 구현하기 (1) (0) | 2022.12.14 |