한동안 친구와 같이 프로젝트를 진행했다. 변리사의 작업을 대신 해주는 프로그램인데, 쉽게 말해 자신이 내고자 하는 특허가 이미 있는 기술인지 검색하는 것을 도와주는 서비스이다. 참고로 해당 프로젝트는 깃허브에 올렸지만, 되도록이면 졸작 박람회 전까지 비공개로 할 계획이다.
지금은 필자의 파트가 거의 끝난 시점이라(유지 보수 단계) 이 글에서는 필자가 프로젝트를 진행하면서 얻은 교훈들을 말하고자 한다.
맡은 파트
필자가 맡은 부분은 서버 파트이다. 엄밀히 말하면 웹 서버와 특허 검색 서버, 머신 러닝 파트를 연결하는 인터페이스 서버이다. 이 서버는 웹서버에서 사용자 입력을 보내면 KIPRIS PLUS(특허 검색 api 서비스)에 특허 데이터를 요청해 XML 데이터를 받아온다. 이 때 특허 데이터는 API 호출 횟수를 줄이기 위해 데이터베이스에 캐시용으로 따로 저장한다. 그 후 자료들을 추합해 CSV 파일로 만든 후 머신 러닝을 실행한다. 머신 러닝 결과는 JSON 형식으로 웹서버로 보내 사용자에게 전달하게 된다. 서버의 환경설정은 지정된 폴더의 YAML 파일을 이용한다.
사용한 기술
언어
우선 서버는 전반적으로 Go언어를 이용해 제작했다. 우선 Go언어가 자신 있는 언어라는 점과 프로젝트의 파트 분리가 명확했기 때문에 추가적인 비용이 없다는 점이 컸다. 머신 러닝을 하기 위한 python 스크립트나 쉘 스크립트 호출에 적합하기도 했고 동시성 프로그래밍 기법도 어느 정도 적용했기에 성능과 개발 속도 양 측을 고려해서 결정했다.
데이터베이스
캐시용으로 사용한 데이터베이스로는 MongoDB를 선택했다. 이 선택은 사실 애매한데, 역시 필자가 써봤던 데이터베이스라 쓴 것이 크다. 지금 생각하면 저장할 데이터가 정형화 되어있어서 스키마가 명확하기 때문에 레퍼런스가 더 많은 sql 계열 데이터베이스가 좋지 않았을까 싶다.
기타
그 외에 쉘 스크립트와 다양한 파일 형식(XML, CSV, JSON, YAML)을 이용했다.
깨달은 점
첫 협업
물론 github에 몇 번 기여도 해보고 팀 프로젝트도 했지만 이렇게 본격적으로 파트를 나눠 팀 프로젝트를 진행한 것은 처음이었다. 같이 일한 친구는 웹/머신러닝 파트를, 필자는 서버 파트를 알고 있다. 하지만 서로 다른 파트에 대한 이해도가 떨어져서 의사소통에 살짝 어려움이 있었다. 다만 이 점은 공통적으로 아는 용어가 아니면 되도록 풀어서 설명하는 것으로 어느정도 해소됐다.
Docker의 필요성
가장 와닿았던 점은 Docker의 필요성이었다. 해당 프로젝트에는 python, golang, 쉘 스크립트 등이 들어가는데 특히 python에는 먼저 설치해야 할 패키지들이 있었다. 특히 친구가 맡았던 머신 러닝 파트로 쓰이는 python에는 venv라는 가상 환경이 있는데 이런 독자적인 생태를 이해하지 못해서 첫 실행부터 애를 먹었다.
사실 이런 상황에서 Docker가 좋은 해결책이라는 것은 이미 알고 있었다. 하지만 Docker를 배우는 비용이 단순히 두 명의 환경을 통일시키는 것보다 더 클 것이라 판단해 따로 적용시키지 않았다. 결과적으로는 잘못된 판단이었다. 여러가지 예상치 못한 문제(환경변수 및 디렉터리, 패키지 등)를 겪으면서 오히려 비용이 더 커졌다. 결국 팀으로 작업할 때는 Docker를 사용해 환경을 통일시키는 편이 낫다는 결론에 이르렀다.
Go언어와 쉘 스크립트
Go언어에서 쉘 스크립트는 각 exec.Cmd 구조체마다 독립적으로 동작한다. 풀어서 쓰자면, 여러 개의 exec.Cmd를 작동시킬 때 각각의 커맨드의 실행 환경은 독립적이다. 당연한 말이라고 생각한다면 다음의 예제를 보자.
cd $HOME/Desktop
mkdir music
package main
import "os/exec"
func main() {
exec.Command("cd", "$HOME/Desktop").Run()
exec.Command("mkdir", "music").Run()
}
첫번째 코드는 쉘 스크립트고 두번째 코드는 Go언어이다. Run()은 객체의 작업을 실행하고 마칠 때 까지 기다리는 함수이다. 보기에 두 명령은 같은 작업을 할 것처럼 보인다.
예상했듯이, 아니다. 두 작업은 서로에게 영향을 미치지 않는다. 첫번째 쉘 스크립트는 $HOME/Desktop으로 이동해 music을 생성할 것이다. 반면 두번째 Go언어 코드에서 한 작업은 Desktop으로 가는 작업을, 다른 작업은 스크립트를 실행한 위치에서 music을 생성하는 작업을 각자 진행할 것이다. 즉, 두 객체는 사용자의 위치를 공유하지 않는다.
처음 Go언어에서 python venv를 쓸 때, 두번째 코드처럼 venv를 실행하는 동작과 python 코드를 실행하는 코드를 분리해서 실행했다. 파이썬을 공부해본 사람이라면 알겠지만 python 코드가 라이브러리를 찾지 못한다는 오류를 일으켰다. 이 문제로 몇 일을 해맸다.
해결법은 간단했다. 명령어들을 묶은 쉘 스크립트를 만들어 exec.Command에는 해당 스크립트를 호출하기만 하면 된다.
package main
import "os/exec"
func main() {
exec.Command("$HOME/initMusic.sh").Run()
}
방법론
TDD 기법을 사용하지 않은 것이 아쉬웠다. TDD 기법이 분명 크게 보면 속도 면에서나 안정성 면에서나 뛰어나다. 문제는 당장 보기에는 느리다는 것에 있고 그렇게 재미있는 작업이 아니라는 것이다. 크게 회귀문제로 데인 것은 없지만 문제가 발생할 때마다 프로그램 전체를 봐야한다는 점이 꽤나 머리아팠다. 무엇보다도, 필자가 TDD의 중요성을 강조한 글을 작성했기 때문에 변명의 여지가 없다. 다음 프로젝트를 진행할 때는 TDD를 의식할 것이다.
마치며...
이번 프로젝트를 진행한다는 핑계로 블로그에 글을 안올렸다. 앞으론 1주에 1번씩 꾸준히 올릴 예정이다.
참고 자료
siyoon210, SQL vs NoSQL (MySQL vs. MongoDB), 2019.3.20