イントロダクション
目下、開発中のプロダクトなので詳しいことは書けないのですが、いろいろと気付きの多い出来事だったので、
少し自分自信の振り返りも兼ねて、投稿してみたいと思います。
これは、決してGoよりPythonのほうが優れているとかそういった話ではないです。
今回、自分は開発者というよりプロジェクトマネージャー(以降、PM)という立場になります。
Goの採用
当社のコア技術はPythonなのですが、今回、開発にあたってGoを採用していました。
主な採用理由としては、「プロトコルとしてgRPCを採用するにあたって、gRPCとの組み合わせ事例が多い」からでした。
gRPCの採用理由は、「同時に企画されていた別プロダクト(Python)との連携が想定されており、異なるプログラミング言語間でも型を維持したままデータ交換が可能」なことからでした。
当初は、プロダクトのリリース時期も未定でプロトタイプ的に実装されていたので、この時点では、社内的に実績のない新規技術を採用しても、特に問題がない状態でした。
問題がなかったかどうか検証するために、そもそも技術選定がどうあるべきか、考えたいと思います。
プロダクト開発にあたって技術選定はどうあるべきか
SIerでの開発経験を経て、自社開発の会社に入ったのでどうあるべきか、というのは分かっていませんでした。
まずは、考えるにあたって比較材料としてSIerでの例を挙げたいと思います。
SIerの場合
例として、SIerの開発の場合は、QCDの観点から実績のある技術を採用します。
これは基本的にSIerのビジネスとしては、
- 発注先が検収してもらえる品質にした上で、要件通りに開発する。(Q)
- 利益がちゃんと出る範囲内で開発コストをかける。(C)
- 決められた納期内に開発を終える。(D)
という前提があるので、保守的な技術選定になりやすいというところでもあります。
極端な話、新規技術を採用した場合は、上記の裏返しとして、
- 発注先が検収してもらえる品質にした上で、要件通りに開発できるか分からない。(Q)
- 利益がちゃんと出る範囲内で開発コストが収まるか分からない。(C)
- 決められた納期内に開発を終えられるか分からない。(D)
となるからです。
分からない=リスクがある、というところですが、このリスクをオフしていくには実績や経験が必要です。
- 社外的に実績のある技術かどうか(=俗にいう枯れた技術)
- 社内的に実績のある技術かどうか(=何かあったときにヘルプしてもらえるか)
- PJ内に経験者はいるかどうか(=プロジェクトをリードするだけの実績や経験があるか)
あとは、社内・社外問わず開発者を集めやすいかどうか、というのも重要です。
これらの要素を考慮して想定されるリスクが許容範囲内だと判断された場合に、その技術は採用されます。
よっぽど発注元からの技術指定があったり、新規技術を採用して検証する場合は、その技術を使うこと自体がプロジェクトの目的になるので、これは別のケースです。
あとは、潤沢な人的・金銭的なリソースがある場合に限り、新規技術を採用できるかと思いますが、受託開発においては、稀だと思います。
自社開発の場合
プロダクト開発にあたっては、2通りあるのかなと思います。
- どんなものを作るのかから検討する段階で、どのぐらいの人数・メンバーでいつまでに開発するか分かっていない。
- QCDの前提条件がはっきりしており、どんなものをどのぐらいの人数・メンバーでいつまでに開発するか分かっている。
2については、SIer同様に判断できるかと思っていて、やはり「分からない=リスクがある」を避けるような技術選択をすることになると思います。
1については、SIerの受託開発だとあまりないパターンで自社開発特有なのかなと思います。
今回、Goを採用した段階ではまさに1の状態で「もやっとしたイメージはあるけど、プロダクトのコンセプトを固めるためにプロトタイプを作ろう」という段階でした。
なので、CostやDeadlineの観点はなく、要件を含んだQualityの観点からのみ判断すればよい状態でした。
かなりリスクをとって、社内的に実績のない新規技術を採用しても、要件に合致しているのであれば、許容される状況です。
そのため「Goの採用後、プロトタイプ的に実装されていた段階においては、問題がなかった」と考えられます。
プロトタイプ開発から本番プロダクト開発へ
Goで実装されていたプロトタイプですが、徐々に本番プロダクト開発が意識されるようになります。
とはいえ、この時点では、「N人ぐらいの体制で社内向けに徐々に開発していく」といった見通しでした。
社内向けに開発していって、社外のユーザーも使える段階に入ったら外部に公開しようという話でした。
しかも社内向けに開発していくが、代替手段はあるので無理のない範囲で開発していくという状態です。
この時点でQCDでいうと、Qがより明確になりつつあり、Cは月々の固定費が決まり、Dについては無理のない範囲でというところでした。
おそらく取りうるリスクのレベル感は、この時点でも下がっていたはずですが、開発メンバーも慣れない技術に手こずりつつも新規技術に触れられる事自体がモチベーションにもなっていたので、選択技術の見直し自体は話題に上がっていませんでした。
多少、gRPCでのWeb開発の先行事例の少なさから手間がかかりすぎている感じはしていたのですが、
そこまで懸念をしていませんでした。
プロダクト戦略の方針転換、リリース日が決まる
「社内向けに開発していって、社外のユーザーも使える段階に入ったら外部に公開」という想定で開発していたのですが、急遽、プロダクト戦略の方針転換が行われ、「○月に向けて開発をし、社外ユーザーを初期ターゲットにする」となります。
この時点でQCDのDが決まったことになります。ただし、Qについてはまだ詳細化されておらず、リリース日の決定が先行した段階だったので、
- Qはおぼろげに見えつつあるが、正直分からない。(=詳細な要件が見えてないので工数が見積もれない)
- Cについては、使えるだけ使う。(=社内・社外から集められるだけの人は集めた)
- Dは○月。
という状態でした。
正直、技術選定のやり直しをすべきかどうかも判断がつかず、プロトタイプ的に作ってきたとはいえ、一部の機能も出来上がっているのでこのまま行くという判断を取りました。
ただ、振り返ってみれば、この時点でPythonに切り替えるという判断は出来たようにも思います。
まず、「プロトタイプ的に作ってきたとはいえ、一部の機能も出来上がっているので」というのは、まさにコンコルド効果でもあって判断の基準にしてはいけなかったと思います。
ここで0ベースで考えたとき、どういった判断や材料があったのか。
まず、当初のGoの採用理由でもあり、gRPCの採用理由は、「同時に企画されていた別プロダクト(Python)との連携が想定されており、異なるプログラミング言語間でも型を維持したままデータ交換が可能」なことからでした。
ここは、どうかというと当初の別プロダクト(Python)企画は見送りとなっており、別の別プロダクト(Python)が動き始めていました。
そうなると、同じなのではという気がしますが、当時と状況が違うのは、片方の企画はリリース日が決まっており、片方の企画は未定という状態です。
おそらくこの時点で技術選定の選定条件が崩れていたことを正しく認識し、0ベースで考えるべきだったと思いますが出来ていませんでした。
一番ポイントなのは、「プロジェクト全体のリスクを判断すべきポジションのメンバーがよく分かっていない技術を使って開発してはいけない」というところかもしれません。
ここは、プロジェクトや会社によってPMが技術を分かってなくてもいいというところもあるかもしれませんが、今回、自分自身がGoとGo周辺の状況をよく分かっていなかったというところが1つポイントのように思うのです。
当時、Goについての印象は、
- Goは、Googleが開発した言語である。
- なんのために開発した言語かは知らない。(=技術の目的を知らない)
- Goは、JavaやC#と同様に型付け言語である。
- JavaやC#の経験がある自分なら分かるだろう。(=過信)
- Goは、採用理由にもあるようにgRPCと相性が良く、マイクロサービス的に考える上で良い。
- マイクロサービスへの過信。(=過信)
でした。
Goを選択したメンバーが諸事情により離脱し、ORMを入れ替える。
この時点で自分も開発メンバーにも加わり、gRPCやGo、ORMについて見直しをしました。結果、以下のメリット・デメリットを判断してORMのGORMのみSQL BOILERに変更をしました。
メリット
SQLBoilerの方がGORMより速い。
- 公式のBenchMarkを確認する限り、GORMを含むその他のORMよりSQLBoilerの方が優秀。
GORMよりSQLBoilerの方が書きやすい
- 主観が含まれる部分なので具体性にかけると思いますが、PythonのDjangoやJavaのJPA系ORMを経験してきたメンバーは、SQLBoilerの方が馴染みやすいかと思います。
- 参考:gormの書き方とsqlboilerの書き方を比較
他のORMからSQLBoilerに移行したという記事はあるが、逆がない。
デメリット
- 単純にGORMのwatch・start・forkとSQLBoilerのwatch・start・forkを比べると、GORMの方が多い。
- とはいえ、スター数や知名度などからメジャーな ORM 感を醸し出しているが、使えば使うほど粗が見えてしまいという意見もある。
メリットでもデメリットでもないところ。
- 自動生成
- SQLBoilerはテーブル定義に基づいてmodelのstructを自動生成する。
- GORMはmodelのstructに基づいてをテーブルを自動生成する。
ORMに関する私見
- JavaやC#で過去にアプリケーションを実装してきた経験上から
- 発行されるSQLが自動生成されるので、ORMを過度に信用するのは良くない。
- 予期せぬ挙動をすることがあり、障害につながるケースがある。
- パフォーマンス的に及第点のSQLになる可能性がある。
- 発行されるSQLが自動生成されるので、ORMを過度に信用するのは良くない。
- というところから、なるべく生SQLを書こうという流れを見てきました。
- とはいえ、開発効率の面から
- 複雑なクエリーは生SQL、それほど複雑ではないクエリーはORMの自動生成
- というのが落とし所です。
- sqlxからSQLBoilerへの移行もその一例かと思います。
gRPCやGo自体については、「リリース想定の時期までを考えるとそこまで見直している時間がない。すでに出来ているものもあるので活かしていく」というやはりコンコルド効果でもあったと思います。また、PMポジションにあるメンバーが実装に加わった時点でプロジェクトが破綻していたともいえます。
これは前職の先輩の教えでもあるのですが、「PMは開発メンバーよりも長期的な時間軸で物事を考えないといけない」という教えがありました。PJのメンバーにはそれぞれポジションによって見るべき時間軸があり、開発メンバーは比較的、短期から中期の時間軸、PMは中期から長期の時間軸で考えるべき、というものです。
PMが実装に加わるということは、短期から中期の時間軸で考える開発メンバーも兼ねるということであり、それはつまり中期から長期の時間軸に対する注意がおろそかになるということでもあります。
Goで開発していたが、途中でPythonに切り替えた。
リリース日が決まっているのでやれるところまでやってやろう、というまったく冷静さのかけた情熱の持ち具合で、自分自身も実装に加わってしばらく開発したところある事件が起きました。
「自分の知らない間にORMのバージョンが上がっていて、しかも動かなくなった箇所がある。
たまたま最新のバージョンで開発していた開発メンバーがいたことで気づいたのですが、特定のDB依存とはいえ、まったく動かなくなった処理がありました。
過去の自分の経験上、JavaやC#であれば、下位互換がされないようなアップデートがORMほどのWebアプリにとって根幹的な技術で起きたことはなく、またあったとしても対処法も合わせて提供されるような世界でした。さらに少数の数名のメンテナーの意思によって技術が左右される世界でもなかったと思います。
SQL BOILERというGORMに比べればマイナーなORMを選択した結果ともいえますが、それでもやはりGORMにしてもDjangoに比べれば、コミュニティのサイズが小さく感じられます。
合わせて、Goで実装していく際に感じるJavaやC#と比べたストレス具合によって、この時点で我に帰り、0ベースで技術選定をし直します。
0ベースで技術選定をし直した結果どうだったか。
結果的にPythonを選択することにしました。
理由は、
- もともと当社はPythonistaの集団であり、Pythonの経験やノウハウはあるが、Goはない。
- GoにもWebフレームワークはあるがまだ未成熟な段階であり、Pythonには十分に成熟したDjangoがある。そして、新規プロダクトはWebアプリケーションである。
- ORMを一例として、コミュニティの充実度がPythonの方が高い。我々がGolangでbuilt a strong ecosystem or communityしていくだけのスキルと覚悟があるなら、Goを選択すべきだが、そうではない。
- 新規プロダクトは当社とって重要なプロダクトである。Goの技術的優位性がない限り、ノウハウのあるPythonで作るべきである。サービス開始までの時間は限られている。
- 経験者の多いPythonで作った方がリリースに向けて、およびリリース以後も要員拡充がしやすく、事業優先度の高いプロダクトに対して手厚く開発体制を確保できる。
- 今後、Goで開発していくことのメリット(処理速度の速さ等)を受けられるようなプロダクトを当社が持っているかというと現状そうではない。
- Goを当社が組織としてキャッチアップしていくことのメリットが現状想定されない。Googleの課題解決(to solve problems which are at the scale of Google, that basically involves thousands of programmers working on large server software hosted on thousands of clusters.)のために開発された言語を当社が使用することの優位性が見当たらない。
でした。
参考にしたサイトとしては、
- なぜ私達は Python から Go に移行したのか
- なぜGo言語 (golang) はよい言語なのか・Goでプログラムを書くべき理由
- Goのメリット、デメリット
- Go vs Python: Select the Best One For Your Business
- Golang vs. Python: Which One to Choose?
- Python vs Go: What's the Difference?
- GO vs. Python: If You Had to Pick One…
となります。
一番のトリガーは何だったのか。
もうこれは自分で実装していて気づいた点なのですが、ORMの充実具合がPythonのDjangoと比べたとき、Goだと十分ではなかったという点です。
以下、参考程度のイメージなのですが、Goで110行程度になる処理がPythonなら60行程度になるというところでした。単純なCRUDのマスターメンテナンスの時点では気づかなかったのですが、JOINするテーブルが2〜3に増えると途端にコードの行数が増えています。
GoのSQL BOILERの場合(GORMのstructを用意する点は変わらない)、
package query
import (
"fmt"
"time"
"golang.org/x/net/context"
"github.com/panair-jp/xxx/go/models"
"github.com/panair-jp/xxx/go/pb"
"github.com/panair-jp/xxx/go/query"
"github.com/volatiletech/sqlboiler/queries/qm"
)
// xxxQuery struct{}
// xxxDataView is custom struct.
type xxxDataView struct {
ID int `boil:"xxx_id"`
xxxID int `boil:"xxx_id"`
Memo string `boil:"memo"`
xxx int `boil:"xxx"`
xxx float64 `boil:"xxx"`
xxx float64 `boil:"xxx"`
xxx float64 `boil:"xxx"`
xxx string `boil:"xxx"`
xxx float64 `boil:"xxx"`
xxx string `boil:"xxx"`
}
// xxxDataViewliceModel is custom model.
type SxxxDataViewliceModel struct {
xxxSlice []*xxxDataView
}
// Fetch xxxQuery is an method that select xxx
func (s *xxxQuery) Fetch(ctx context.Context, xxxID int, targetYear int) ([]*pb.xxxData, error) {
xxxIds := make([]interface{}, 1)
xxxIds[0] = xxxID
var utilsQuery = query.UtilsQuery{}
xxxIDs, err := utilsQuery.GetxxxIDs(ctx, xxxIds)
if err != nil {
fmt.Printf("%v", err)
return nil, err
}
targetYearFrom, targetYearTo, err := utilsQuery.Getxxx(ctx, xxxID, targetYear)
if err != nil {
fmt.Printf("%v", err)
return nil, err
}
var result = new(xxxDataViewliceModel)
var queries = getxxxDataQueries(xxxIDs, targetYearFrom, targetYearTo)
err = models.NewQuery(queries...).BindG(ctx, &result.xxxSlice)
if err != nil {
fmt.Printf("%v", err)
return nil, err
}
pbxxxData := []*pb.xxxData{}
for _, xxx := range result.xxxSlice {
var xxx, xxx, xxx = Getxxx(xxx)
pbxxxData = append(pbxxxData, &pb.xxxData{
Id: uint64(xxx.ID),
xxxId: uint64(xxx.xxxID),
Memo: xxx.Memo,
xxx: uint64(xxx.xxx),
xxx: xxx,
xxx: xxx,
xxx: xxx.xxx,
xxx: xxx,
xxx: xxx.xxx,
xxx: xxx.xxx,
})
}
return pbxxxData, nil
}
func getxxxDataQueries(xxxIDs []interface{}, targetYearFrom time.Time, targetYearTo time.Time) []qm.QueryMod {
var queries []qm.QueryMod
{
queries = append(queries, qm.Select(
" xxx.id as xxx_id, "+
" xxx.xxx_id as xxx_id, "+
" xxx.memo as memo, "+
" xxx.xxx as xxx, "+
" xxx as xxx, "+
" xxx.xxx as xxx, "+
" xxx.xxx as xxx, "+
" xxx.xxx as xxx, "+
" xxx.xxx as xxx, "+
" xxx.xxx as xxx "))
queries = append(queries, qm.From("xxx xxx "))
queries = append(queries, qm.InnerJoin("xxx xxx on xxx.id = xxx.xxx "))
queries = append(queries, qm.InnerJoin("xxx xxx on xxx.id = spgcef.xxx "))
queries = append(queries, qm.WhereIn("xxx.xxx_id in ? ", xxxIDs...))
queries = append(queries, qm.And("xxx.\"timestamp\" between ? and ? ", targetYearFrom, targetYearTo))
queries = append(queries, qm.OrderBy("xxx.memo, spg.\"timestamp\", xxx.xxx_id "))
}
return queries
}
// Getxxx provide xxx from xxx or xxx.
func Getxxx(xxx *xxxDataView) (float64, string, string) {
var xxx float64
var xxx string
var xxx string
var xxx = xxx.xxx
if xxx == int(pb.xxx) {
xxx = xxx.xxx
xxx = xxx.xxx
xxx = "xxx" + xxx.xxx
}
if xxx == int(pb.xxx) {
xxx = xxx.xxx
xxx = xxx.xxx
xxx = "xxx" + xxx.xxx
}
if xxx == int(pb.xxx) {
xxx = xxx.xxx
xxx = xxx.xxx
xxx = "xxx" + xxx.xxx
}
return xxx, xxx, xxx
}
Python&Djangoの場合、
from xxx.models import *
from xxx.pb import xxxData
def fetch(xxx_id, target_year):
xxx_ids = [xxx_id]
xxx_ids = get_xxx_ids(xxx_ids)
target_year_from, target_year_to = get_target_year_from_to(xxx_id, target_year)
xxx_list = Xxx.objects.filter(
xxx_id__in=xxx_ids,
timestamp__gte=target_year_from,
timestamp__range=(target_year_from, target_year_to),
).order_by('memo', 'timestamp', 'xxx__xxx_id')
pb_xxx_data_list = []
for xxx in xxx_list:
xxx = xxx
xxx = xxx.xxx
xxx = xxx.xxx
xxx, xxx, xxx = get_xxx(xxx, xxx)
pb_xxx_data = xxxData()
pb_xxx_data.id = xxx.id
pb_xxx_data.xxx_id = xxx.xxx_id
pb_xxx_data.memo = xxx.memo
pb_xxx_data.xxx = xxx.xxx
pb_xxx_data.xxx = xxx
pb_xxx_data.xxx = xxx
pb_xxx_data.xxx = xxx.xxx
pb_xxx_data.xxx = xxx.xxx
pb_xxx_data.xxx = xxx.xxx
pb_xxx_data.xxx = xxx.xxx
pb_xxx_data.append(pb_xxx_data)
return pb_xxx_data_list
def get_xxx(xxx):
xxx = xxx.xxx.xxx
if xxx == xxx:
xxx = xxx.xxx
xxx = xxx.xxx
xxx = "xxx" + xxx.xxx
elif xxx == xxx:
xxx = xxx.xxx
xxx = xxx.xxx
xxx = "xxx" + xxx.xxx
elif xxx == xxx:
xxx = xxx.xxx
xxx = xxx.xxx
xxx = "xxx" + xxx.xxx
else:
raise ValueError("想定外の計算方式です")
return xxx, xxx, xxx
となります。
ここでじゃあ、Djangoと同じように使えるORMを使えばいいのでは、というところでbeegoという選択肢をPJメンバーから出してもらったのですが、ここで0ベースで考えたとき、やはりSIerのときと同様にQCDを考慮した場合に実績のある技術を採用する、という判断を取りました。
最後に
プロダクト自体はまだリリースされておらず、目下、開発中です。
じゃあ、この判断自体の成否も判断できないのでは、というところですが、リリースに間に合ったからといって正解とも言えないように思うのです。
ただ、そうそう会社単位で頻繁に起こりうる判断でもないと思いますし、記憶の新しいうちに書き留めておくことで多少なりとも解像度を高く、今回のことを振り返ればと思って記載しました。
良くも悪くも世の中のエンジニアの参考になればと思います。