2018年上半期振り返り
今年も半分終わってしまった。
やったこと
レガシーシステムのリニューアル
入社してからずっとやってたシステムリニューアルがやっと(一部)リリースできた。
10年弱放置されてたシステム一からリニューアルして、クリティカルなバグ出さずにリリースまで持ってったわい凄くない?
— すーさん二号 (@suusan2go) 2018年6月28日
10年放置は言い過ぎで、このツイートに追加してるけど「10年くらい前に作られて数年放置されてた」システムが正しい。たくさんアクセスがあるタイプのシステムではないが、事業的には結構重要なシステムだったのでここをほぼトラブルなしで移行できたのは結構嬉しかった。まだ古いサーバで動いてるバッチを移行したりとかは残ってるし、明日出社して月初のバッチでおかしなことになってたりしたら辛いが・・・・・・
全体の設計もして、サーバサイドでDDDでAPI作って自分でそれを受けるSPAも書くみたいなことほぼ一人でやってたので、途中死にそうになったがなんとかここまでこれてよかった。QAの人がいる開発組織は初めてなんだけど、リリースの前に細かいところまで見てもらえたので大きな問題を起こさずに済んだと思う。
とはいえまだ全部終わったわけではなく、上記のバッチとかいろいろあと一ヶ月くらいはやることあるので頑張りたい。
Slackいれた
今の会社に入社して、とにかくツールまわりでテンション下がっていたのでいれたやつ。いれたのは正確には昨年末だけど。
半年くらい一部エンジニアにだけ展開されていたが、晴れて開発に関わる人ほぼ全員には使ってもらえることになりそう。Slackいれた当初は、元から入ってたツールとの価格差から内部のエンジニアからでさえ「コストメリットあるのか?」みたいなことを言われたりもして死にたくなったが、今では割となくてはならない存在になっている感ある。この辺はやはり外部ツールとのインテグレーションやbotが簡単に作れるとかあるけど、単純にツールとしての手触りが良いというのはあるなとは思う。ずっと当たり前に使ってたので気が付かなかったけど、emojiリアクションは偉大。
導入にあたってはいろいろ社内政治的なところも見えたりしてテンションさがったりしたが、まあここまでこれたので良しとしよう。
他にもドキュメントツールについて、他の人が音頭を取って導入を進めてくれることになって大変ありがたい。自分もいれたいと常々思っていて比較表とか作ったりしていたんだけど、Slack導入前後の色々面倒なアレで踏ん切りがつかなかったので。
採用やった
あんまり書けることがないけど、採用に関わった。放置されてた採用ツールをちゃんと運用したり、スポンサーのとりまとめしたりした。
人を評価するのって難しいが、いろんなエンジニアの人と話ができるのは結構楽しかったりする。
RubyKaigiのスポンサー準備は初めての経験で大変だったが楽しかった(小並感)
学んだこと
静的型付け言語(Kotlin)でDDDでWEB APIを書く
前職ではずっとRubyでRailsだったので(ちょっとしたツールにGoを書いたりはしたけど)、Kotlin + Springでサーバーサイド、単純なMVCではなくオニオンアーキテクチャでWEB APIを書くのはとても勉強になった。
エリック・エヴァンスのドメイン駆動設計は読んだことがあったものの、実際のコードを書くところには中々紐付けられなかったので、実践ドメイン駆動設計も買って読んだ。
結果として、自分の設計能力は格段に上がった感じがしている。意識が高まってRailsのサービスクラスについて、勉強会で発表するなどした。
Vue.jsでSPAを作る
Reactは業務で書いたことがあり、SPAも某社のコードテストで書いたことがあったものの、業務でVue.js、SPAを書くのは地味に初めてだった。
SSRは必須ではなかったけど、全体的な設計の拠り所が欲しくてNuxtを選んだ。当時はまだβではあったのでリスクもあったけど、これは割と正しい判断だったんじゃないかという気がする。Vue.jsは前職であまり評判がよくなく、どうかなーという印象だったんだけど、使ってみるとかなり手触りがよかった。
サーバAPIとのつなぎこみ部分で型が欲しくてTypeScriptを選択したが、これも割といい判断だった気がする。Nuxt + TypeScriptを使う開発についても意識が高まって勉強会で発表したりした。
余談だけどこの発表資料により、某ペイ的な会社でNuxt + TypeScriptの採用が進んだという噂を観測していて嬉しい。
gRPC
色々あって結局プロダクションでは使わないことになったんだけど、途中までgRPCを本番導入する予定だったので色々勉強してた。
「フロントエンドエンジニアも知っておきたいgRPC」の方は自分史上過去最高のブクマがついて嬉しかった。なんかフロントエンドって入ってると伸びる気がしている。裾野が広いからだろうか(別に狙ってつけたわけではないけど)。
まとめと近況とか
振り返ると、この半期は普通に仕事でやって学んだことを勉強会でシュッと発表できている。今の会社に入って辛い面もあったけど、技術的な幅を広げたいという転職時に考えていた希望は達成されていることに気がついた。
今まで出会ったことがないタイプの優秀な人、特にCSに強い人が多いのでその辺りはかなり刺激になっている。自分はもともと物理系の出身で、プログラミングも研究のなかで必要だったのでやっただけ。基本的な足回りが弱いという自覚はあるのでやっていきたい。
Twitterで文句ばっかり言ってるせいか、退職の波動 がすごく出ているらしい。が、別に転職活動はしていないし、いくつかお話はぶっちゃけあったが、そういう意思決定はしていない。
一方で、直近前職を辞めた人たちが殆ど、数人〜十数人規模のスタートアップに行っていて、それは正直ちょっと羨ましく思っている。
昨日の飲み会で前職を退職した人何人かと話して、大体みんなSOをそれなりに貰える数人ー10人規模のベンチャー行ってて、みんないいチャレンジしてんなーと思っての発言でし
— すーさん二号 (@suusan2go) 2018年6月30日
た https://t.co/xZtPLi0ulz
(SO含め)一発当てたいのもあるし、システム・組織が小さいところから事業・会社を大きくしていく経験はどこかでやりたい。
とはいえ、上の子はまだ小さいし、奥さんも子供産んでから体調壊し気味だし、二人目もできたしーという状況で、本当に創業期に入っていくのは結構辛いだろうし、そしてそれは数年は変わらんだろうなーと思う。なので、なんかこうちょうどいいバランスでよいタイミングがあったら、挑戦したいなと漠然と考えたりしている。
SmartHRのエンジニアの入社歓迎会の練習をする会に行ってきた #saizeriya_meetup #smarthr
SmartHR社主催の「エンジニアの入社歓迎会の練習をする会」に行ってきました。
会場の様子
歓迎(の練習)されて来る【SmartHR】エンジニアの入社歓迎会の練習をする会 https://t.co/bfCo9Y0LYX #saizeriya_meetup
— すーさん二号 (@suusan2go) 2018年6月11日
気合い入り過ぎだろww pic.twitter.com/eu0qtSpJpB
— すーさん二号 (@suusan2go) 2018年6月11日
入社おめでとうございますと言われて混乱している
— すーさん二号 (@suusan2go) 2018年6月11日
前職では何されてたんですかとかいう質問が飛び交っている…
— すーさん二号 (@suusan2go) 2018年6月11日
ワインをボトルで頼んでいます pic.twitter.com/AzDRdcrF4q
— すーさん二号 (@suusan2go) 2018年6月11日
SmartHR、これまで退職者3名なのか
— すーさん二号 (@suusan2go) 2018年6月11日
しらねぇwww pic.twitter.com/tuzmswF71t
— すーさん二号 (@suusan2go) 2018年6月11日
— すーさん二号 (@suusan2go) 2018年6月11日
SmartHRにエンジニアが入らない理由、半分くらいは六本木の会社のせいだったのでみんな安心してSmartHRに入社してくれよな
— すーさん二号 (@suusan2go) 2018年6月11日
新入社員の自己紹介コーナーで、みんな最初は戸惑いながら、ネタで前職では…って話してたのが、途中から皆さんスラスラと前職では…って話してて、あれみんな転職したんだっけという気持ちになりました https://t.co/WOJ1iNn9mP
— すーさん二号 (@suusan2go) 2018年6月11日
感想
歓迎会といってもサイゼリヤで飲み放題、食べ放題みたいな感じだろうなーと思ってたら、席に風船が用意されてたり、名刺が用意されてたり、会場についたときの挨拶も「入社おめでとうございます!」だったり、入社おめでとうのTシャツが配られたり、本当に 歓迎会 でした。
あくまで本当に歓迎会で、決して強引な採用トークもなく、楽しくエンジニアリングやSmartHR社の中身について教えていただけたのが本当に素敵だったなーと思います。こういう企画がエンジニアの定例?的なところから実現されたというのもイイですね。
なんでこんなにいい会社っぽいのに採用に苦労しているかというと
- そもそもエンジニア採用がだいぶ厳しい状況である(自分も採用に関わっているのでわかる)。
- 会社の規模的に、ジュニアすぎるエンジニアは採用しても育成する余裕がない。
- toBのサービスのため候補者にサービスの魅力を伝えるのが難しい。toCの会社と競合すると厳しい。
などがあるそうです。どちらかというとサービス・プロダクト指向なので、とんがった技術を使っていきたいという技術指向の方はあまりマッチしないかもともおっしゃっていました。
サイゼリヤ
仕組みはよくわかりませんが、コース?でいろいろでてきてビビりました。料理も全部美味しく、クソでかいワインボトルとか出てきて、飲みすぎました。また行きたいので、各社サイゼリヤでミートアップしてください。
まとめ
サイゼリヤ最高。SmartHR良い会社っぽい。うちもSmartHRいれてくれ(前職ではry)。
Meguro.rbでRailsのサービスクラスについてLTしてきました
初めていったMeguro.rbでLTしてきました。本当はすぐブログ書きたかったんだけど、当日は睡眠不足でめちゃくちゃ眠く、翌日は飲み会で、土日は子供を寝かしつけると同時に寝落ちしてしまったのでこんなに遅くなってしまった。
SpringでDDD的構成をやって、なんとなくRailsのサービスクラスってこういう感じにするべきじゃね?というのを掴んだつもりだったんだけど、いざ資料にまとめてみると全然まとまらず、何が言いたいのかよくわからなくなり、なんか冗長な感じになってしまった気がする。そして資料を詰め込めすぎて、後半早口になってしまった。
LT後も反応が薄かったので、やべーこと言ってしまったかなと思ったんだけど、インターネッツにあげた資料自体への反応は悪くなくてよかった。
言いたかったこと
Twitterの反応であーそうですこれです結局言いたかったのは…となったのはこういうことでした。
結局のところ、サービスクラスという手段に飛びつくのではなく、ちゃんと考えてモデリングしようよってことなんだよね。 https://t.co/3LUJWtpjzP
— Hideki Igarashi (@ganta0087) 2018年5月25日
要は使い方だよね。他のクラスとの責務分割がハッキリしていれば問題ない。なんとなーくロジック寄せたりしてるからよく分からんコードになる。責務分割の方針によってはサービスクラスが不要になることだってある。
— pospome (@pospome) 2018年5月25日
Railsのサービスクラス周りの記事、とにかく手段について着目している記事が多くて、そういうものへのアンチテーゼみたいなのも裏テーマとしてありました。
サービスクラスに限らない話ですが、責務とかモデリングをよく考えた上で導入していかないと、結局コードが散らばって辛いだけになるということです。 trailblazer
みたいなGemもありますが、ツールありきで考えてしまうと、結局のところあまり良い結果にはならないんじゃないかなと。
trailblacer github.com
Meguro.rbについて
Twitterでしか知らなかった人と何人か顔を合わせてお話することができてよかったです。 一番の進捗はこれで、割と真面目にちょっとやばい人だと思ってたら、本当にいい人でした。人柄という意味でもエンジニアという意味でも。
なんでそんな自虐的なHNなんだろう、きっとやばい人だと思ってた方が、すごくまともな人で、そのHNもちゃんとヤバくない意味があって安心した
— すーさん二号 (@suusan2go) 2018年5月24日
目黒は帰り道なので、またネタがあれば参加したいです。よろしくおねがいします。
#roppongijs でgRPC-WebについてLTしてきた
gRPC-WebについてLTしてきた。 仕事では結局使わなかったんだけどgRPCをブラウザで使うにはということを色々と調べていたので、それについて話した。
suzan2go.hatenablog.com suzan2go.hatenablog.com
スライドでも触れたけど、公式?のgrpc/grpc-webはまだまだ色々と辛い感じなので、今後の発展に期待という感じである。 本当はTODOくらいVueかReactで作っておこうと思ったんだけど、公式のdockerイメージが全然ビルドできなくて断念。
gRPC-Webがまだ早いと思った人もいたみたいだけど、こっちのgRPC-Webはツールもこなれてるし全然問題ないんじゃなかろうか。
今回はフォントの話とか、Nodeの話とか、幅広く色んな話が聞けてよかった。相変わらずメルカリさんの会場、軽食ともすごくよかったー。次回も予定があえば参加したい。
Railsのコントローラでの不可解(に見える)なエラーハンドリングについて
やっぱ家でもなるわ。適当なcontrollerに
— snocchy (@snocchy) 2018年4月19日
def index
raise ActiveRecord::RecordNotFound # (1)
rescue
raise 're-raise' # (2)
end
とか書いてブラウザから開くと何故か(1)で死んだことになってる。コンソールから叩くと(2)になる。raiseするのをActiveRecordErrorにすればどちらも(2)
Twitterでこのようなやりとりを見かけて、気になって眠れなかったので調べてみた。 長ったらしく説明しているので結論だけ知りたい方は下までスクロールしてください。
問題
手元ですぐ動かせる Rails5.1.1 / Ruby 2.5.0で試した。
# controller class WelcomeController < ApplicationController def index raise ActiveRecord::RecordNotFound rescue => e raise 're raise' end end # config/route.rp Rails.application.routes.draw do root to: 'welcome#index' end
この状態でWEBにアクセスするとこうなる!!
!? raise re raise
にならん・・・まじだ
rescueされてないようにみえる。
ちなみにこれをコンソールでやるとこうなる
コンソールだとちゃんと “re raise” が例外になる?!
次にコントローラの例外をRecordNotFoundでなくしてみる
# controller class WelcomeController < ApplicationController def index raise ActiveRecordError rescue => e raise 're raise' end end
するとちゃんと re raise
の方が例外として上がってくる…不可解
種明かし
rescueされてないよう見えるが、本当にされてないのだろうか。
binding.pry
を仕掛けてみよう
class WelcomeController < ApplicationController def index raise ActiveRecord::RecordNotFound rescue => e binding.pry raise 're raise' end end
あれ・・・ちゃんとrescueされてますね・・・ ということは、画面上の表示がおかしいのだろうか
lib/action_dispatch/middleware/debug_exceptions.rb @ line 68 ActionDispatch::DebugExceptions#call: 57: def call(env) 58: request = ActionDispatch::Request.new env 59: _, headers, body = response = @app.call(env) 60: 61: if headers["X-Cascade"] == "pass" 62: body.close if body.respond_to?(:close) 63: raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" 64: end 65: 66: response 67: rescue Exception => exception => 68: raise exception unless request.show_exceptions? 69: render_exception(request, exception) 70: end [1] pry(#<ActionDispatch::DebugExceptions>)> exception => #<RuntimeError: re raise>
render_exception
というエラーページを生成しているところまでちゃんと re raise
のエラーが渡ってきていますね。
では render_exception の中身を見ていきましょう
# actionpack-5.1.1/lib/action_dispatch/middleware/debug_exceptions.rb def render_exception(request, exception) backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) log_error(request, wrapper) if request.get_header("action_dispatch.show_detailed_exceptions") content_type = request.formats.first if api_request?(content_type) render_for_api_request(content_type, wrapper) else render_for_browser_request(request, wrapper) end else raise exception end end
ExceptionWrapper.new
が怪しそう。さらにこのコードを追ってみます。
# actionpack-5.1.1/lib/action_dispatch/middleware/exception_wrapper.rb module ActionDispatch class ExceptionWrapper # 〜〜〜省略〜〜〜〜 @@rescue_responses.merge!( "ActionController::RoutingError" => :not_found, "AbstractController::ActionNotFound" => :not_found, "ActionController::MethodNotAllowed" => :method_not_allowed, "ActionController::UnknownHttpMethod" => :method_not_allowed, "ActionController::NotImplemented" => :not_implemented, "ActionController::UnknownFormat" => :not_acceptable, "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, "ActionDispatch::Http::Parameters::ParseError" => :bad_request, "ActionController::BadRequest" => :bad_request, "ActionController::ParameterMissing" => :bad_request, "Rack::QueryParser::ParameterTypeError" => :bad_request, "Rack::QueryParser::InvalidParameterError" => :bad_request ) # 〜〜〜省略〜〜〜〜 def initialize(backtrace_cleaner, exception) @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end # 〜〜〜省略〜〜〜〜 private def original_exception(exception) if @@rescue_responses.has_key?(exception.cause.class.name) exception.cause else exception end end # 〜〜〜省略〜〜〜〜 end
読んでいくと以下のような動きになっていることがわかります。
initialize
でoriginal_exception
というのを呼ぶ、original_exception
ではexception.cause
で元々発生していた例外を見に行く- それが
rescue_responses
に一致する場合には、それをExceptionWrapper
のexception
にセットする
でも、ここには ActiveRecord::NotFound
に対応するものはありません。
実はここではなくて、ActiveRecod
の方で拡張が行われているようです。(コードちゃんとおったわけではないけど多分これ)
# activerecord-5.1.1/lib/active_record/railtie.rb module ActiveRecord # = Active Record Railtie class Railtie < Rails::Railtie # :nodoc: # 〜〜〜省略〜〜〜〜 config.action_dispatch.rescue_responses.merge!( "ActiveRecord::RecordNotFound" => :not_found, "ActiveRecord::StaleObjectError" => :conflict, "ActiveRecord::RecordInvalid" => :unprocessable_entity, "ActiveRecord::RecordNotSaved" => :unprocessable_entity ) # 〜〜〜省略〜〜〜〜 end
ここまで来てやっとわかりました。例外のその前に発生していた例外が、ActiveRecord::RecordNotFound
の場合には、画面上に発生した例外そのもの(今回でいうと re raise
)ではなく、元々の例外であるActiveRecord::RecordNotFound
が表示される。という挙動になっているようです。
結論
実際にTweetされてる方の環境を確認したわけではないので完全なる推測ですが、以下のような状況なのではないでしょうか
- エラーハンドリングはきちんと行われている
- しかし特定の例外の場合には、rescue節の例外ではなく、その前に発生していた元々の例外の情報が画面に表示される
- 上記の挙動により
ActiveRecord::RecordNotFound
がrescue
されていないような不可解な挙動に見えた
ちなみにこの挙動はRails3.2.0RCからあるもののようです。
追記
もともとの実装はこっちか https://t.co/DGtctOgyYz
— すーさん二号 (@suusan2go) 2018年4月20日
僕も気になったので調べてみたんですが元のチケットとコミットはこれですねhttps://t.co/yTBxoHM9f6https://t.co/lM1Y0LdoaA
— わかば (@wakaba260yen) 2018年4月20日
何らかの例外をラップした例外を返しても、大本のエラーがrescue_responseの対象として登録されてたらrescue_responseを適用するようにするようにしているみたいです
#roppongijs でNuxt.js + TypeScriptの話をしてきた
仕事で使っている Nuxt.js + TypeScriptの話をしてきた。本当はもっと話したいことあったんだけど、5分なのでTypeScriptに絞って話しをした。 第一回目の発表ということもあって結構レベルの高い発表が多かったんだけど、懇親会では何人かから質問もあったしまあまあ良かったのではと思っている。
SpringからAPIクライアントを自動生成する仕組みのところが皆さん気になっていたようで、結構Swagger定義を手で書くのしんどいっすって話が多かった。自分の例ではSpringのControllerの実装からSwaggerの定義は自動で作っているので、Swaggerは手で書いていない。手で書くのは正直結構しんどいよねーと思います。懇親会で話してたら結構 grpc-web
とか grpc-gateway
の話に興味を持つ人が多かったので、その辺も機会があればLTしようかな。
あと、フロント専任のエンジニアみたいな人が多いのかなと思いきや、割と大きい会社の人でも結構サーバーと兼任みたいな人の方が多くて意外だった。会社の規模が一定以上になると、サーバー / フロントを分けるのが普通なのかなと思ってたけどそうでもないんですね。自分としてはどっちもやっていきたいので、そういう会社が多いと嬉しいな。
メルカリさんの会場は広いし綺麗だし、ミートアップ用の冷蔵庫あったりですごかった。また4月にあるようなので是非参加したい。
本番運用まで行かなかったgRPCの知見をまとめておく
会社のブログに書こうと思ったんだけど、ちょっとマイナスイメージを持つ人もいそうな気がしたので、個人ブログに書くことにした。
この3ヶ月くらい、システムのリニューアル(アプリ間で分散したロジックを集約するバックエンドサーバと、用途に応じたフロントエンドサーバを立てるみたいなマイクロサービス構成)をやっていて、そこでサーバ間のやりとりにgRPCを使っていた。すごーく雑な絵を書くとこんな感じです。
しかし、最近になってプロジェクトのスコープについて見直しが入りました。マイクロサービス化ではなく単純にレガシーJavaで独自FWなアプリをリプレースするだけになり、必要なのはSPAとSpringBootのAPIサーバだけに(要するにRails側のロジックをなんとかするのがスコープ外になった)。
で、SPAに提供するAPIのためにgRPC(+ grpc-gateway)を使うのはちょっとオーバースペックだよねーという話になり、gRPCをやめて普通にRESTのAPIを作ることになりました。*1
プロジェクトが終わったら色々知見を公開したいなとおもったんだけど、その機会がなくなってしまい、ちょっと勿体無いのでブログにしておきます。 あくまでプロジェクト都合でgRPCを使うのをやめただけなので、何か問題があってとかではないです。その辺を期待してこの記事を開いた人はスミマセン。
このポストで話すこと
Spring BootでgRPCサーバーを作る
Javaコードを生成する
gradleのプラグインを使いましょう
なぜかドキュメントに書いてない気がするんだけど、 ./gradlew generateProto
で .proto
からコードが生成できます。
コンパイル時に必ずこれが走るようにしておくとよいでしょう。最初は雑に生成したコードもGitの管理に含めてしまってましたが、割と差分が馬鹿にならない量になっていくので、.gitignore
しておくことをオススメします。
クライアントコードを生成する
ruby / gateway用のGoコード、さらにcom.google.protobuf
で公開されているような Timestamp
などを使おうと思うと、これらをビルドする依存関係をインストールするだけでも
かなり大変になってきます。
protoeasy
というツールを使うと、このあたりの生成が以下のように大分楽になります(公式のDockerImageも提供してくれています)。
protoeasy --go --grpc --go-import-path github.com/user/your-go-project --cpp --ruby . # exclude protocol buffers files in foo/*
ただ2018年3月13日現在では公式のDockerイメージではSwagger定義を生成することができないので、独自のDocker Imageを作って対応しましょう。 --grpc-gatewa-swagger
オプションはよ。
このツールを使って、コアとなるAPIサーバがmasterにマージされたら各クライアントライブラリと後述するgrpc-gatewayサーバのリポジトリを更新しにいくということをしていました。
私は大本となるgRPCのサーバで.proto
ファイルも管理してしまっていましたが、このように色んなものをCIでビルドしようなどと思うと結構辛くなってきそうなので、クライアントが増えてきたら.proto
だけ配布して、そっちでビルドしてくれ!って世界観もありかなと思います。
.proto
ファイルの依存関係を解決してくれるようなツールは標準では私が知る限りないのですが、CyberAgentの方が protodep
というツールを作成されているようです。
Spring BootでgRPCを動かす
Spring BootでgRPCを動かすまでは本当に簡単です。 springboot-starter
というライブラリが公開されており、ほぼそれで完結します。
こちらは過去Qiitaにも書いたので、よかったら見てみてください。
.protoファイルの整理について
シンプルなサーバであれば、 .proto
ファイル一つで事足りるかもしれませんが、アプリケーションが大きくなってくると、
一つの .proto
ファイルでは見通しが悪くなってきます。
じゃあどう整理したらいいのかというところですが、私はgoogleが公開しているGCP用の .proto
ファイルの構成を参考にしました。(ちなみに結構ファイルによって書き方がマチマチ…w)
例えば bigtableの .proto
ファイルは以下の様に3つに分割されています。
googleapis/google/bigtable/v1 at master · googleapis/googleapis · GitHub
bigtable_data.proto -- Request / Responseで使うmessage定義 bigtable_service.proto -- Service定義 bigtable_service_messages.proto -- ServiceのRequest / Responseの定義
自分は最終的に以下のように整理しました。
src/main/proto/todo L todo_service.proto L request L get_todo_request.proto L post_todo_request.proto L response L post_todo_response.proto L get_todo_response.proto
認証をどのように行うか
このブログで紹介されているように、Interceptorを使ってSpring SecurityのSecurity Contextに認証情報を詰め込むという方法を取っていました。
で、最初は割とうまく行ってたんだけど、普通に動かしてる分にはよくても、Spring Securityまわりでテストが落ちることが頻発したりとちょっと挙動が不安定に。
Spring Security allows us to specify an alternative SecurityContext store by implementing a custom SecurityContextHolderStrategy. Additionally, the gRPC Java runtime provides the Context class, which can be used to carry state across API boundaries and between threads.
と、あるように grpc-java の Context
を使って無理やり実装してみたらテストの不安定さは解消されたものの、Securiy Contect
の機構に頼らず、素直にgrpc-java のContext
を使ったほうが良さそうだなぁという感触です。
エラーハンドリングについて
エラーハンドリングについてはこちらに書きました。
例えばバリデーションのような詳細な情報を返したい場合にはMetadataに詰めて返すようにしていました。
grpc-javaのサンプルにもあるように、以下のようにすると良いでしょう。
Metadata trailers = new Metadata();
trailers.put(DEBUG_INFO_TRAILER_KEY, DEBUG_INFO);
responseObserver.onError(Status.INTERNAL.withDescription(DEBUG_DESC)
.asRuntimeException(trailers));
gRPCをアプリケーションのどのレイヤーにおくか
クライアント、サーバで型を共有できるのが、gRPCの良いところではありますが、gRPCで生成されたクラスを所謂ドメインレイヤーまで引きずるような設計にはしないほうが無難です。
この場合gRPCのモデルからドメイン層、アプリケーション層にもってくるときのマッピングがかなり面倒になります。が、gRPCに強く依存する形でアプリケーションを作ってしまうと、今回のようにgRPCを差し替えないといけないことになったときに大変な事になります。
今回意識してgRPCにアプリケーションロジックが依存しないように作っていたので、gRPCからRESTへの書き換えも1週間程度で終わりました。
参考までに、プロジェクトの構成はざっくり以下のようにしていました。
domain/ application/ presentation/ └ grpc/ ├── interceptor/ └── service/ infrastructure/
grpc-gateway について
grpc-gatewayを使用して、SPA向けのRESTのエンドポイントを作成していました。grpc-gatewayのためのコード生成時にSwagger定義を同時に生成することができるので、Swagger Codegenを使ってフロントエンド用のTypeScriptクライアントも同時に生成して配布していました。
grpc-web
という選択肢もあったのですが、社内にAPIアグリゲータがいるとか、パスを見てnginxでリクエストを振り分ける必要があるとか色々あり、ブラウザから叩かれるエンドポイントとしてはgrpc-gatewayを選択しました。
grpc-gatewayはエンドポイントとしてのコードを自動で生成してくれるだけなので、実際のGoのサーバーはある程度自分で書く必要があります。逆にいうと足りないものがあれば拡張してくことが可能です。
エラー情報をクライアントにJSONでいい感じに返す
grpc-gateway
ではデフォルトでエラーの内容が以下のようになります。
{ "error": "invalid token", "code": 16 }
上記で書いたように、 Metadata
に詰めた内容も grpc-gateway
でクライアントにJSONとして返したい場合には、エラーの場合の挙動をカスタマイズする必要があります。
実はこの方法は、grpc-gatewayのwikiにリンクが貼られている以下のブログに書いてあります。
runtime.HTTPError
にエラーハンドラーが定義されているので、これを差し替えます。
色々端折っていますが、自分は以下のようなエラーハンドラを定義しました。
// CustomHTTPError おもにgRPCのMetadataをerrorBodyのような構造に変換し、クライアントに返すためのカスタムハンドラー func customHTTPError(ctx context.Context, _ *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, _ *http.Request, err error) { const fallback = `{"error": "failed to marshal error message"}` w.Header().Set("Content-type", marshaler.ContentType()) w.WriteHeader(runtime.HTTPStatusFromCode(grpc.Code(err))) eb := errorBody{ Err: grpc.ErrorDesc(err), } md, _ := runtime.ServerMetadataFromContext(ctx) for k, v := range md.TrailerMD { eb.ErrorDetails = append(eb.ErrorDetails, errorDetail{ Field: strings.TrimSuffix(k, "-bin"), // バイナリで帰ってくる文字列は-binのprefixがつくので、クライアントが扱いやすいよう消す Message: string(v[0]), // サーバー側では文字列としてしか入れていないが、何故かArrayで入ってくるので最初のものだけ取得する }) } jErr := json.NewEncoder(w).Encode(eb) if jErr != nil { w.Write([]byte(fallback)) } }
ファイルアップロード / ダウンロードする
以下のIssueにもあるように multipart form request
は grpc-gateway
では使えません。
一番簡単な方法は、Base64エンコーディングして受け渡しをしてしまうことです。
ただしProtocol Buffersの説明にもあるように、Protocol BuffersではMB以上のサイズを受け渡しするのは向いていません。
https://developers.google.com/protocol-buffers/docs/techniques
手元でやったところ大体3MB以上のファイルになると、この方法ではうまく行きませんでした。この場合には、grpc-gatewayに独自のエンドポイントを設けて 一度gatewayでファイルを受けて、Client Streamingで送るとかを検討する必要があるかもしれません。※自分はそれほど大きなファイルを送らずに済んだので、試してませんが…
JSONのMarshallをカスタマイズする
grpc-gateway
はデフォルトでは、Protocol Buffersでデフォルト値になっているものをレスポンスから省いてしまいます。
これはproto3では、デフォルト値なのか値がセットされていないのかを区別できないのでこのような挙動になっているようです。
この挙動は以下のIssueのコメントにもあるように WithMarshalerOption
で変える事ができます。
gwmux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: true, EmitDefaults: true}))
- OrigName
- デフォルトではJSONはキャメルケースになりますが、これをtrueにすると、
.proto
に定義されたフィールド名で出力されるようになります
- デフォルトではJSONはキャメルケースになりますが、これをtrueにすると、
- EmitDefaults
- proto3でデフォルト値になっているものもJSONで出力することができます。
まとめ
いまいちまとまりのないポストになってしまいましたが、プロジェクトで触った、gRPC / grpc-gateway について書きました。
個人的にはgRPCはかなり開発体験が良かったので、次に機会があればまた検討したいなと思います。