Golang ORM, 무엇이 좋을까?
Go언어로 프로그램을 작성하다 보면 가장 고민스러운 부분 중 하나가 Data Layer 구성 일것이다. 웹 서버를 구성할 때 대부분 DB와 통신을 할 텐데, 아직 type-safe 하면서, 많은 star를 가지고, 잘 maintain되는 killer library가 없기 때문이다. 시중에는 참 많은 대안들은 존재한다. 쉽고 강력한 full-featured ORM 기능을 제공하는 gorm
, xorm
은 runtime에 타입을 비교하여 type-safe 하지 못하고, 표준 라이브러리 database/sql
이나 sqlx
는 너무 low-level interface라 많은 어플리케이션 코드를 직접 짜야 하는 단점 등 내 마음에 딱 맞는 라이브러리는 찾기가 어려웠다. 내가 생각하기에 이상적인 Data layer 조건은
- 기본적인 DB Operation은 표준 라이브러리
database/sql
과 호환이 가능해야 한다. 그렇지 않으면 라이브러리를 교체하기가 상당히 어려울 것이다. - 라이브러리가 일정 궤도에 올라있어야 한다. GitHub 기준 1000개의 스타가 넘어 사용자 풀이 단단해야 하고, 라이브러리가 액티브하게 관리되고 있어야 한다.
- Database First. Database First는 데이터베이스의 필드를 기준으로 코드에 적용시키는 방법이고, Code First는 Go code를 먼저 작성하면 그에 맞추어서 DB가 변경되는 방식이다. Code Frist는 마이그레이션을 자동으로 해주어 위험하며, 어떻게 동작할 지 정확하게 이해하지 않으면 서버만 띄워도 DB가 변조될 수 있어서 Database First 방식이어야 한다.
- Domain 모델을 더럽히지 않아야 한다. Domain 모델에 struct tag나 기타 method를 강제해서는 안된다. Domain 모델이 더러워지면 확장성이 떨어지고, side effect가 생길 수 있다.
- No magic. 정적으로 코드가 작성되어 type-safe하며 컴파일 레벨에서 디버그 가능해야 한다. 런타임에서 생길 수 있는 오류를 최소화 해야 한다.
- DB의 테이블 구조를 변경할 때 마다 따로 작성해야 하는 코드를 최소화하기. DB에 테이블을 추가하거나, 필드에 변화를 줬을 때 따라서 같이 변경해주어야 하는 게 너무 많으면 놓칠 수 있기 때문에 관리포인트를 줄이기 위해 수동 코드작업은 최소화되어야 한다.
- 여러 DB 종류를 지원해야 한다. 최소 MySQL과 PostgreSQL을 지원하여, RDBMS 종류만 바꿨음에도 코드를 싹 갈아엎어야 하는 상황은 최소화되어야 한다.
이렇다. 까다롭다고 생각할 수 있지만, 안전하고 편안하게, 또 확장성 있는 코드를 작성하기 위해서는 필요한 조건들이다.
가장 먼저 살펴 보았던 것은 ORM 대표주자 xorm
과 gorm
이다. 둘 다 굉장히 풍부한 star 수, 두터운 사용자층, 잘 정리된 documentation을 가지고 있다. 두 라이브러리의 치명적인 점은 런타임에 타입을 추론하고, Field Mapping을 한다는 것이다. 내가 작성한 도메인 모델이 DB와 잘 매핑이 되었는지 런타임에, DB Operation을 직접 수행하고 나서 야 알수 있다면, 너무 위험하지 않을까? 이를 피하기 위해서 도메인 모델의 struct tag에 DB의 어떤 필드와 매칭되는지를 표시할 수는 있지만, 도메인 모델을 더럽혀야 하고 DB의 테이블 구조를 변경할 때 마다 적절히 struct tag를 변경해야 한다는 점이 찝찝했다. 하지만 위의 사항만 괜찮다면, 둘은 좋은 옵션이다. 역시 많은 사용자층에는 이유가 있는 법.
그와 비슷했던 것이 upper.io
였다. 이는 위의 xorm
과 gorm
과 비슷하지만 MongoDB와 CockroachDB 와 같은 다양한 DB를 지원한다. 하지만 위와 같은 이유로 일단은 나에게 맞지는 않았다.
그 다음으로 찾아본 것은 sqlx
이다. 이 라이브러리 역시 9천개의 스타를 받은, 수많은 사람들이 사용한 명 라이브러리이다. 이 라이브러리는 표준 라이브러리 database/sql
를 확장시킨 개념으로, 편리한 기능들이 많이 추가되었다. 하지만 이 라이브러리는 단독으로 사용하기에는 여전히 low level query만을 지원하여 구현해야 할 부분이 너무 많고, 특히 도메인 모델을 직접 사용하기에는 적절하지 않았다. 하지만 low level query를 직접 사용하면 되는 작은 규모 프로젝트에서는 좋은 선택이라고 생각한다.
이후 entgo
라는 프레임워크를 살펴보았는데, 이 프레임워크는 필요한 모델 (Entity라고 한다)의 스펙을 Go로 짜면, 모델 및 쿼리들을 auto generate 해주는 형식이다. Code를 만들어주는 방식이기 때문에 magical한 부분이 없으며, 이 프레임워크가 기본적으로 각 모델간의 연결을 잘 정의해주므로 아주 RDBMS에 잘 맞는 형태이다. 정말 좋은 후보라는 생각이 들었지만, 나에게 아쉬웠던 부분은 Database First가 아닌 Code First라는 점 이었다. 물론 DB Migration에 대한 훌륭한 설명 이 있으니, Code First여도 괜찮은 사람들이라면 아주 좋은 솔루션 중 하나 일 것이다. 심지어 Facebook에서 만드는 라이브러리이므로, 관리는 잘 될 것이다 :)
그리고 sqlc
와 xo/xo
를 살펴보았다. 둘 다 SQL을 입력하면 상응하는 Go Code를 생산한다 는 시스템을 가지고 있었다. sqlc
는 테이블 스키마까지 SQL로 넘겨주어야 하고, xo/xo
는 테이블 스키마는 DB에서 읽고, Custom SQL은 직접 넘기는 방식이었다. 둘 다 Go Code를 만들어주기에 type-safe하다는 장점이 있다. 하지만 각각 다 실제로 사용하기에는 아쉬운 부분이 있었는데, sqlc
는 나의 도메인 모델과 sqlc
가 만든 모델을 쉽게 연결할 수 있는 방법이 없었고, xo/xo
는 현재 코드가 관리되고있지 않아서 신규로 도입하기에는 아쉬운 상태였다. sqlc
는 도메인 모델을 매핑 문제를 해결하거나 sqlc
가 만들어준 모델을 사용한다면 큰 문제없이 사용 가능할 것 같다.
또 sqlboiler
라는 라이브러리를 살펴봤는데, 이 라이브러리도 DB를 읽어 Go code를 만들어주는 라이브러리이다. sqlc
와 다른점은 sqlboiler
가 생성해주는 코드에 템플릿을 넣어줄 수 있다는 점이다. 템플릿을 통해서 기존 도메인 모델과 sqlboiler
모델의 정적 매핑 함수를 만들어줄 수 있기 때문에, 도메인 모델 문제가 해결되는, 더 나은 솔루션이다. 세팅이 약간 복잡하다거나, 예시가 없어 코드 작성이 약간 어려울 수는 있으나 문서가 방대하여 사용에는 문제가 없다.
이외에도 go-pg
, squirrel
등 더 많은 라이브러리를 리서치하였지만, go-pg
처럼 특정 RDBMS만 지원하거나 squirrel
처럼 inactive 상태인 라이브러리들은 위 목록에서 제외하였다.
이렇게 찾아본 후, 내가 최종적으로 선택한 것은 sqlx
와 sqlboiler
이다. 두개인 이유는 sqlx
는 DB 커넥션, 트랜잭션 등의 low-level 파트를 맡고, sqlboiler
는 high-level 파트를 맡아서 사용하고 있다. 아직 많이 사용해보지는 않았지만, 이 둘의 조합으로 Data layer를 원하는 조건에 맞추어서 사용해보기로 결정했다. 아직 많이 사용해보지는 않아서 후기는 따로 없지만, 위의 리서치가 여러분의 Go언어 ORM 라이브러리를 고르는데 도움이 되었기를 바란다.