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 대표주자 xormgorm 이다. 둘 다 굉장히 풍부한 star 수, 두터운 사용자층, 잘 정리된 documentation을 가지고 있다. 두 라이브러리의 치명적인 점은 런타임에 타입을 추론하고, Field Mapping을 한다는 것이다. 내가 작성한 도메인 모델이 DB와 잘 매핑이 되었는지 런타임에, DB Operation을 직접 수행하고 나서 야 알수 있다면, 너무 위험하지 않을까? 이를 피하기 위해서 도메인 모델의 struct tag에 DB의 어떤 필드와 매칭되는지를 표시할 수는 있지만, 도메인 모델을 더럽혀야 하고 DB의 테이블 구조를 변경할 때 마다 적절히 struct tag를 변경해야 한다는 점이 찝찝했다. 하지만 위의 사항만 괜찮다면, 둘은 좋은 옵션이다. 역시 많은 사용자층에는 이유가 있는 법.

그와 비슷했던 것이 upper.io 였다. 이는 위의 xormgorm과 비슷하지만 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에서 만드는 라이브러리이므로, 관리는 잘 될 것이다 :)

그리고 sqlcxo/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 상태인 라이브러리들은 위 목록에서 제외하였다.

이렇게 찾아본 후, 내가 최종적으로 선택한 것은 sqlxsqlboiler이다. 두개인 이유는 sqlx는 DB 커넥션, 트랜잭션 등의 low-level 파트를 맡고, sqlboiler는 high-level 파트를 맡아서 사용하고 있다. 아직 많이 사용해보지는 않았지만, 이 둘의 조합으로 Data layer를 원하는 조건에 맞추어서 사용해보기로 결정했다. 아직 많이 사용해보지는 않아서 후기는 따로 없지만, 위의 리서치가 여러분의 Go언어 ORM 라이브러리를 고르는데 도움이 되었기를 바란다.