IntelliJプラグインでAnnotate機能を作った

はじめに

これは Ubie Advent Calendar 2019 - Qiita の21日目の記事です。

想定読者

(開発方法セクションより前)

(開発方法セクション以降)

本題

まずはこれを見て欲しい。

https://raw.githubusercontent.com/shiraji/find-pull-request/master/website/images/list-pr.gif

以前から欲しいなーって思っていたのだけど、プライベートの時間の捻出が出来ず、主に眠気に勝てないのが原因で、なかなか腰が上がらなかったんですが、息子くんがすんなり寝てくれて2時間くらい時間が出来たので、さくっと作ってみた。その後Twitterに成果報告して寝た。

翌朝起きたら中々の反応がもらえてたので、ちょっとやる気が出て、そのまま勢いで作り切りました。余談ですが、自分の場合、こんな機能作った!ってツイートした場合、反応あるとすごくモチベーションが上がるので、欲しいな!って思ったらぜひいいねしてもらえると!

これ単体でプラグインとして出しても良いのですが、切り出すメリットがあまり見えなかったため、Find Pull Request プラグインのv1.7.0の機能として出しています。Repositoryはこちら

この機能により、Find pull requestプラグインは3つの機能を持つことになりました。

  1. 右クリックでpull requestページに飛ぶ
  2. 右クリックでpull requestページのURLをコピーする
  3. pull requestの一覧をエディタに記載する

まだまだ他にも欲しい機能があるので、ちょくちょく作っていこうと思います。他にも欲しい機能あればぜひissueにあげてください。ちなみにGitHub, GitHub Enterprise, GitLab, Bitbucketで動作します。他にも欲しいサービスあれば、issueテンプレート埋めていただければ対応します。

開発方法

せっかく作ったので、調べた知見を置いておきます。

ここから想定読者は 「IntelliJプラグインをがっつり開発したことがある・Annotate機能を作りたい人」とします。つまり基本的なプラグイン開発の説明は省略します。

参考ソース

普段プラグイン開発するなら他の似たようなプラグインソースコード読め。で終わるのですが、アイコンつけるのはいくつかありましたが、Annotate機能を使ったプラグインは誰も作ったことがないのではないか?と言うくらい見つけることが出来ませんでした。

その為、みなさんお馴染みのintellij-communityのrepoから仕様を把握することにしました。

github.com

その中でも唯一Annotate機能を提供している、 AnnotateToggleAction を見て開発を行ないました。

https://github.com/JetBrains/intellij-community/blob/master/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java

Find pull requestプラグインは開発当時2019.1をサポートしているので、masterブランチではなく、2019.1のソースを見て開発していました。後述するけど、これが後に問題になります。

Annotateを出すには

色々読んだ結果、Annotateを出すには editor.gutter.registerTextAnnotation() を呼び出すことで実装が可能のようです。

  /**
   * Adds a provider for drawing custom text annotations in the editor gutter, with the
   * possibility to execute an action when the annotation is clicked.
   *
   * @param provider the provider instance.
   * @param action the action to execute when the annotation is clicked.
   */
  void registerTextAnnotation(@NotNull TextAnnotationGutterProvider provider, @NotNull EditorGutterAction action);

引数は二つ、一つ目の TextAnnotationGutterProvider はどんなテキストを出すのか?もう一つの EditorGutterAction はそのテキストに対してクリックした時どんなアクションを起こすのか?です。クリックなどのアクションが必要ない場合、二つめの引数は省略可です。

TextAnnotationGutterProvider

TextAnnotationGutterProviderで一番重要なメソッドはこの getLineText メソッドです。

/**
   * Returns the text which should be drawn for the line with the specified number in the specified editor.
   *
   * @param line   the line for which the text is requested.
   * @param editor the editor in which the text will be drawn.
   * @return the text to draw, or null if no text should be drawn.
   */
  @Nullable
  String getLineText(int line, Editor editor);

このメソッドは引数に行番号と表示するEditorが渡されます。戻り値にはその行に表示するStringです。

簡単なメソッドなのですが、一つ問題があります。それはこのメソッドが呼び出されるタイミングはAnnotateがレンダーされ得るたびと言うことです。

その何が問題なのかを説明する前に、Annotateの機能についておさらいします。AnnotateとはEditorの左横の部分を右クリックし、Annotateを選択したら、その行の最後のコミット(正確には少し違う)を表示します。

f:id:shiraji:20191215165849p:plain
Annotate

ここまではよく知られています。問題はエディタでの表示と言うことはAnnotate表示中も開発者がコードの更新をすることが可能です。つまりコード編集してもAnnotateを閉じず、正しい行に正しい情報を表示する必要があるということです。

f:id:shiraji:20191215170639g:plain
コードの変更・改行・行削除などにも対応する必要がある

registerTextAnnotation するのはアクションがcheckになった時のみです。そのため、registerTextAnnotation された時と getLineText が呼び出された時の表示されているエディタの状態は異なる可能性があることになります。

これを踏まえて、 getLineText の実装をします。最初に問題になるのが、 line の意味です。the line for which the text is requested. と記述されていますが、説明が圧倒的に足りていません。というか、説明通りに受け取ってはいけません。この lineAnnotate が表示された時の行番号に当たります。今まさにEditorで表示されている行番号ではありません。受け取った line をそのまま利用するのは危険です。 line が今表示されているエディターの行番号としてどこに当たるのかを確認し、その確認した行番号に対して情報を返さないといけません。(ここを読むような訓練されたプラグイン開発者なら「あーよくあるやつね」となるいつものやつです。)

line が今表示されているエディターの行番号のどこに当たるのか」は UpToDateLineNumberProviderImpl を利用して取得します。

        val upToDateLineNumberProvider = UpToDateLineNumberProviderImpl(editor?.document, editor?.project)
        val currentLine = upToDateLineNumberProvider.getLineNumber(line)

UpToDateLineNumberProviderImpl#getLineNumber にはドキュメントはありませんが、0未満の時には現在表示されているエディタにはその行は存在していないことを表しています。そのため今回は、0未満の値だった場合、表示されることはないため、空文字即returnにしてあります。

ここが呼び出される回数は思った以上に頻度が高いです。しかも各行呼び出されます。このプラグインではgitのハッシュ値からpull requestの番号を取得しますが、都度その処理を行うことは危険です。そのため、ハッシュ値とPR番号のマッピングregisterTextAnnotation する前に計算しておき、このメソッド内ではそのマッピングから表示する文字列を生成する簡単な処理に留めてあります。

EditorGutterAction

EditorGutterAction重要なのがdoAnnotateメソッドです。

/**
   * Processes the click on the specified line.
   *
   * @param lineNum the number of line in the document the annotation for which was clicked.
   */
  void doAction(int lineNum);

このメソッドのlineNumは現在表示されている行番号になります。 UpToDateLineNumberProviderImpl は利用する必要はありません。Editorは受け取れないことに注意してください。

TextAnnotationGutterProvider と同じようなデータやロジックを使います。そのため、今回の機能では、一つのクラスで二つとも実装することにしています。

FileAnnotation

AnnotateのようにFileAnnotationに依存するような情報を表示する場合、注意しなくてはならないのが、IntelliJのエディタ以外の部分からもファイルの変更が可能になる点です。何を言っているのかわからないと思います。例えばですが、ファイルの変更をして、コンソール上でコミットをするようなケースを考えてください。エディタの変更だけではなく、IntelliJ外部からのFileの変更(FileAnnotationの変更)などを検知し最新の情報を表示する必要があります。ちなみにAnnotateを開いたまま、改行し、コンソールでコミットしてみてください。Annotateが自動で閉じます。今回の機能もFileAnnotationに依存しているため、その変更を検知する必要があります。あまり深く仕様を考えるのは大変であるため、Annotateと同じようにFileAnnotationの変更が発生した場合、Annotateを閉じるようにします。

FileAnnotationの変更を検知するメソッドは二つあります。

  /**
   * Notify that annotations should be closed
   */
  public synchronized final void close() {
    myIsClosed = true;
    if (myCloser != null) {
      myCloser.run();

      myCloser = null;
      myReloader = null;
    }
  }

  /**
   * Notify that annotation information has changed, and should be updated.
   * If `this` is visible, hide it and show new one instead.
   * If `this` is not visible, do nothing.
   *
   * @param newFileAnnotation annotations to be shown or `null` to load annotations again
   */
  @CalledInAwt
  public synchronized final void reload(@Nullable FileAnnotation newFileAnnotation) {
    if (myReloader != null) myReloader.consume(newFileAnnotation);
  }

  /**
   * @see #close()
   */
  public synchronized final void setCloser(@NotNull Runnable closer) {
    if (myIsClosed) return;
    myCloser = closer;
  }

  /**
   * @see #reload(FileAnnotation)
   */
  public synchronized final void setReloader(@Nullable Consumer<? super FileAnnotation> reloader) {
    if (myIsClosed) return;
    myReloader = reloader;
  }

上記のようにコンソールでコミットする場合、FileAnnotateのcloseが呼び出されるため、setCloserメソッド内でAnnotateを閉じる処理を記述します。

        fileAnnotation.setCloser {
            UIUtil.invokeLaterIfNeeded {
                if (!project.isDisposed) editor.gutter.closeAllAnnotations()
            }
        }

同じようにsetReloaderメソッドを実装しようとしたのですが、思いつく限りの行動をしても、残念ながらreloadメソッドが呼び出されることがありませんでした。そのため、setReloaderの実装することができませんでした。AnnotationToggleActionから引数のFileAnnotationが最新のFileAnnotationのようですが、ここは謎です。。。誰か教えて。

2019.1 vs 2019.2

さてここまで実装して、よっしゃリリースだ!って思ったのですが、 closeAllAnnotations メソッドが気になっていました。このメソッド呼び出されるとAnnotateの部分に表示されている情報全てを閉じます。つまり、Annotateを表示し、List pull requestを表示し、 close annotationを選択するとAnnotateとList pull requestが両方とも閉じてしまします。片方だけ閉じることが出来ません。

f:id:shiraji:20191220231636g:plain

2019.1の場合、個々のAnnotateを閉じる方法が提供されておらず、さらに設定されているTextAnnotationの取得が行えません。残念だなーと思っていたのですが、ふとした拍子にこんなissueを見つけてしまいました。

Annotate action behave wrong if other annotations added https://youtrack.jetbrains.com/issue/IDEA-209722

このissueによると、192.2300から個々のAnnotateを閉じることができるようになったみたいです。つまり2019.1では出来ないけど、2019.2では出来ると。2019.1のソースコードばかり追っていたため、最新のコードでは出来る事を出来ないと諦めてしまっていました。

古いバージョンに引っ張られて、しょぼい機能を提供するのはなんだかなーって思ったので、2019.2での開発に切り替えることにしました。

ちなみにこのissueにはこんなことが書かれています。

We've run into that in the students' project, but it also can be actual for 3rd-party plugins developers.

要約すると「学生のプロジェクトやってる時に発覚した。3rdパーティのプラグイン開発者にも発生する可能性あるよ。」とのことです。3rdパーティのプラグイン開発者でAnnotate作って公開まで持っていったの、ひょっとすると相当少ないようです。どうりで似たようなプラグインが見つからない訳ですね・・・

2019.2対応

2019.2(ここでは192.5728以上とします)では新たに以下の二つのメソッドが提供されました。

  @NotNull
  List<TextAnnotationGutterProvider> getTextAnnotations();

  void closeTextAnnotations(@NotNull Collection<? extends TextAnnotationGutterProvider> annotations);

getTextAnnotations はそのEditorに設定されている全てのTextAnnotationsを取得し、 closeTextAnnotations は引数に渡された複数のTextAnnotationを閉じるメソッドです。

これを使えばOK!ということでさくっと実装してみました。上記の問題が解消されると思ったのですが、Exceptionが出ます。

java.lang.Throwable: Synchronous execution on EDT: /usr/bin/git -c ...
    at com.intellij.openapi.diagnostic.Logger.error(Logger.java:145)
    at com.intellij.execution.process.OSProcessHandler.checkEdtAndReadAction(OSProcessHandler.java:117)
    at com.intellij.execution.process.OSProcessHandler.waitFor(OSProcessHandler.java:62)
    at git4idea.commands.GitTextHandler.waitForProcess(GitTextHandler.java:145)
    at git4idea.commands.GitHandler.runInCurrentThread(GitHandler.java:383)
    at git4idea.commands.GitImplBase.doRun(GitImplBase.java:172)
    at git4idea.commands.GitImplBase.run(GitImplBase.java:148)
    at git4idea.commands.GitImplBase.runCommand(GitImplBase.java:46)
    at git4idea.commands.GitImpl.runCommand(GitImpl.java:41)

これは何かというと、UIスレッド時にgitのような外部コマンド叩くとエラーになるということです。IntelliJ 2019.2からこのような仕様に変わったようです。

Find Pull Requestプラグインは各所でgitコマンドを叩いているため、これの対応が必要になります。

外部コマンドを叩く場合、一番簡単な対処方法はバックグラウンドスレッドでの処理にしてしまうことです。バックグラウンドスレッドでの実行方法はいくつかあるのですが、簡単な方法として、Task.Backgroundableを使ってみます。

        object : Task.Backgroundable(project, "Listing Pull Request...") {
            override fun run(indicator: ProgressIndicator) {
                // 外部コマンドを叩くなどの処理+その後続処理
            }
        }.queue()

queue()の呼び出しを忘れがちになるので、注意してください。

さぁ対応終わったぞ!って思ったのですが、今度は別のエラーが出ます。

java.lang.Throwable: Read access is allowed from event dispatch thread or inside read-action only (see com.intellij.openapi.application.Application.runReadAction())
    at com.intellij.openapi.diagnostic.Logger.error(Logger.java:162)
    at com.intellij.openapi.application.impl.ApplicationImpl.assertReadAccessAllowed(ApplicationImpl.java:1027)
    at com.intellij.openapi.editor.impl.EditorImpl.assertReadAccess(EditorImpl.java:3254)
    at com.intellij.openapi.editor.impl.EditorImpl.getSettings(EditorImpl.java:920)

今度は何かっていうと、上でやったバックグラウンドスレッドはEditorへのRead権限がないため、あかんよってことです。上のバックグラウンド処理は必須であるため、read権限あるスレッドでEditorへのアクセスをするように修正します。

以下の公式ドキュメントにスレッド周りの話が書いてありますので、興味ある人は読んで下さい。 www.jetbrains.org

ドキュメントによると今回の場合は、invokeLaterを呼び出せば対処可能になります。つまりコードとしてはこんな感じに。

        object : Task.Backgroundable(project, "Listing Pull Request...") {
            override fun run(indicator: ProgressIndicator) {
                // 外部コマンドを叩くなどの処理+その後続処理
                ApplicationManager.getApplication().invokeLater {
                    // バックグラウンドスレッド内でeditorへアクセスするなどread権限が必要な処理を実行
                }
            }
        }.queue()

ようやく全てのコードがエラーなしで2019.2で動くようになりました。ほっと一安心。

さぁリリースです。

はいリリース後にユーザが多い、Android Studioの最新版3.5系(2019.1ベース)のサポートを勢い余って切ったことが発覚しました。最近Android開発してなかったし、完全に忘れてました。。。

複数バージョン対応

かと言って、2019.1のサポートに戻すと、問題解消されている2019.2のユーザにも問題ある挙動を強制する事になってしまいます。そこで、複数のバージョンのプラグインをリリースすることにしました。

  • 1.7.0 -> 2019.2以降のユーザ
  • 1.7.0-2019.1 -> 2019.1.xのユーザ(Android Studio含む)

対処方法は簡単で、plugin.xmlの idea-version のuntil-buildを設定し <idea-version since-build="191" until-build="192.5727"/>プラグインのバージョンを新しく 1.7.0-2019.1 として、リリースするだけです。(until-buildなどはplugin.xmlじゃなくて、gradle-intellij-pluginで設定可能になっているはずです。未検証) そうするとこんな感じで、IntelliJのバージョンによって、インストールできるバージョンが変わるようになります。

f:id:shiraji:20191221002340p:plain
複数バージョンがホストされる

まとめ

先日のUbieアドベントカレンダーLukas (@cvguy84) | Twitter さんのIntelliJプラグインの記事では

qiita.com

抽象的で考えるとIntelliJプラグインの開発は簡単です。

どこがやー!って思うのですが、

IntelliJプラグインは誰でも作ることができます。 Documentationを慎重に読んで、実装するしかないです。でもそれがOSS開発の一つの楽しみであると思います。

本当にこれです。ドキュメント(という名のソースコード)を読んで、うまく動いた時、すっごく楽しいし、それが人に使われると思うとやめられませんなーってなります。

ちなみに年内に出したいと思ったので、コピペの嵐でやり過ごしました。そのため、2019/12/21現在のrepositoryのコードは参考にしない方が良いです。。。時間とってがっつりフルスクラッチします。誰にも何も言われず、費用対効果ガン無視で思いっきり俺の考えた最強のコードがかけるのもOSSの醍醐味だと思います。

Spring+GraphQLからKtor+GraphQLに移行する

はじめに

これは Ubie Advent Calendar 2019 - Qiita の7日目の記事です。

6日目は かみな/Kaito Minatoya@Ubie (@kamina_zzz) | Twitter による 地球上の Kubernetes ユーザーは絶対使うべきツールたちを紹介するよ - Qiita でした。

想定読者

  • Spring + Kotlin + GraphQLを使っていて、Ktorに移行したくなっちゃった人 (需要🤔)

本題

UbieでAdvent CalendarをKtor縛りで書くぞ!となりました。じゃあ自分は何を書こうかなーって考えてたらGraphQLに決まってるじゃん?と振られたので、UbieではKtor+GraphQLは使ってないのですが、もしSpringのアプリケーションである https://github.com/ubie-inc/kotlin-graphql-sample をKtorに移し替えるとしたら何をするのかを調査しつつ、記事にすることにしました。

この記事では一旦動くところまでを目指します。

生かすコード、諦めるコード

フレームワークを載せ替えるとしてもそこまでガッツリ書き直す訳ではなく、生かすことが出来るコードが多々あります。今回は出来うる限りコードを生かす方向にしました。そのため、graphql-java-toolsを同様に採用します。

github.com

これを採用するため、domainquery resolver, resolver はSpringのアノテーションを消すだけで再利用出来ます。

import com.coxautodev.graphql.tools.GraphQLResolver
- import org.springframework.stereotype.Component

- @Component
class DiseaseResolver(...) : GraphQLResolver<Disease> {

jdbc も同様に修正するのですが、Springのjdbcを使っているため、思った以上にガッツリ修正が入ります。この辺りはKtorで利用するDBアクセスライブラリ次第になります。ここではあまり関係ないので、省略します。

スキーマファイルもSpringの時同様にresources直下に置きます。

再利用できるコードもありますが、完全に捨てるコードもあります。

例えば、JdbcConfigKotlinGraphQLSampleApplication これらはSpringのための設定なので再利用は出来ません。ただこのようなコードだけなのでほとんどのコードが再利用することが可能となります。

新規実装

IntelliJのウィザードで作成されるここから、新規の実装をしていきます。

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
}

最初にRoutingの定義です。 GraphQLなので /graphql にコードを書いていきます。GraphQLは GETとPOSTのサポートをしているので、一応、両方とものAPIを書きます。

fun Application.module(testing: Boolean = false) {
    routing {
        get("/graphql") {
            TODO()
        }

        post("/graphql") {
            TODO()
        }
    }
}

最初にリクエストをパースします。リクエストはこんなJSONです。

{
    "query":"{\n  drug(id:\"id\") {\n    name\n  }\n}",
    "variables":null,
    "operationName":null
}

これをこのままdata classにします。

data class GraphQLRequest(
    val query: String = "",
    val operationName: String? = "",
    val variables: Map<String, Any>? = mapOf()
)

Ktorでデフォルトで使える、 ObjectMapper を使ってパース処理を書くと。

fun Application.module(testing: Boolean = false) {
    routing {
        get("/graphql") {
            val stringRequest = call.receive<String>()
            val mapper = ObjectMapper()
            val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        }

        post("/graphql") {
            val stringRequest = call.receive<String>()
            val mapper = ObjectMapper()
            val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        }
    }
}

何度も同じ処理を二箇所に書くのは辛いので、 GraphQLController を作成します。

class GraphQLController {
    suspend fun handleGraphQL(call: ApplicationCall) {
        val stringRequest = call.receive<String>()
        val mapper = ObjectMapper()
        val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        TODO()
    }
}

これを使ってコード量減らします。

fun Application.module(testing: Boolean = false) {
    // Controller
    val graphQLController = GraphQLController()

    routing {
        get("/graphql") {
            graphQLController.handleGraphQL(call)
        }

        post("/graphql") {
            graphQLController.handleGraphQL(call)
        }
    }
}

さて次は実際に graphql-java を利用してコードです。

    val filePath = "kotlingraphqlsample.graphqls"
    val graphQLSchema = SchemaParserBuilder()
        .file(filePath)
        .resolvers(
            listOf(diseaseResolver, 
                DiseaseQueryResolver(diseaseService),
                // 他のResolverもここに記述する
                ))
        .build()
        .makeExecutableSchema()
    val graphQL = GraphQL.newGraphQL(graphQLSchema).build()

この graphQL インスタンスをController内で使い、それぞれのResolverにアクセスしていきます。graphQL インスタンスはDI使うなり、コンストラクタに差し込むなり、Controller内でインスタンス化するなり、好きなようにControllerに差し込みます。

    suspend fun handleGraphQL(call: ApplicationCall) {
        val stringRequest = call.receive<String>()
        val mapper = ObjectMapper()
        val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        val context = ConcurrentHashMap<String, Any>()
        // graphQLは上でインスタンス化したもの
        val result = graphQL.execute(
            ExecutionInput.newExecutionInput()
                .query(request.query)
                .operationName(request.operationName)
                .variables(request.variables ?: emptyMap())
                .context(context)
        )
        TODO()
    }

最後にresultをJSONにして、返します。

    suspend fun handleGraphQL(call: ApplicationCall) {
        val stringRequest = call.receive<String>()
        val mapper = ObjectMapper()
        val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        val context = ConcurrentHashMap<String, Any>()
        val result = graphQL.execute(
            ExecutionInput.newExecutionInput()
                .query(request.query)
                .operationName(request.operationName)
                .variables(request.variables ?: emptyMap())
                .context(context)
        )
        val json = mapper.writeValueAsString(result.toSpecification())
        call.respondText { json }
    }

これでAPIは完成。APIアクセスを簡単にするために、GraphiQLも導入します。 graphiql/packages/examples/graphiql-cdn at master · graphql/graphiql · GitHub この辺りのサンプルコードをそのまま拝借し、index.htmlとしてプロジェクト直下に置きます。

fun Application.module(testing: Boolean = false) {
    // Controller
    val graphQLController = GraphQLController()

    routing {
        static("/graphiql") {
            // GraphiQL
            default("index.html")
        }

        get("/graphql") {
            graphQLController.handleGraphQL(call)
        }

        post("/graphql") {
            graphQLController.handleGraphQL(call)
        }
    }
}

Runボタン押して、実際にGraphiQL http://localhost:8080/graphiql にアクセスしてみます。

f:id:shiraji:20191207224439p:plain

見えるようになりました。yay!

終わりに

いかがでしたでしょうか?(一回やってみたかった)

やってみて思ったんですが、Springですでに動いているのであれば、今のところKtorに移す必要ないなーと言うのが感想です。

今回はこんなレポジトリのコードを参考にしました。

あまりアクティブな開発がされていないので、今後どうなるのかわからないですが、もうちょっとKtorのGraphQL周りのプラグインが出来たら良いなーと。

自分で作れってことか 🙃

カナダから来たUbieのインターン生(Hiroくん)はすごかった

最初にこの記事はどの企業も個人も批判する目的はありません。もし批判を感じる文章があればご指摘して頂けると幸いです。


Hiroくんインターンを予定通り8月中旬まで無事に終わらせたので、彼がこの期間何をやったのか?などを書こうと思います。

f:id:shiraji:20190825222847p:plain
アイキャッチ画像のための無意味なスクショ

出会い

最初の彼との出会いは、Twitter上で、自分の経歴とほぼ同じ経歴を持っている学生がいるなー珍しいなーと思ったけど、関わりないだろうなーとスルーしたのを覚えています。

その数日後、彼のツイートは炎上していました。

朝ごはん食べてたら前見つけた子が炎上してるじゃん!あららーもっと先にフォローして、声かけちゃえば良かったかなー。と朝ごはん食べながら眺めてました。

最初、インターン先探しているということに対して、声かけるのはなしだよなーって思ったんですが、子供の朝ごはんを少しこぼしたので片付けをしていて、人ってミスするし、そこでダメって決めつけるのおかしい。優秀な子がこれでチャンス無くすのおかしい。弊社のバリューの一つが「launch x launch」(ロンロン)*1なのにグジグジ考えて行動起こさないのおかしい。と思い、声かけることにしました。

f:id:shiraji:20190826000401p:plain
声かけると決めた時の分報

声かけたあと、東京に来るとのことだったので、その時に会いましょうと言う約束をしました。予定を聞いて、最後の方ってことだったので、まぁ彼のキャリア的にも弊社はまだ魅力的には見えないだろうから優先度下げられたかーとちょっと残念な気分でした。

会ってみて

5月頭に彼は日本に一時帰国していて、会社に来てもらいました。 最初会ったとき、顔と体のギャップがすごい!って思ったのですが、話してみると本当に素晴らしい好青年でした。 素直に話は聞けるし、しっかりと意見出来ていたし、キャリアもしっかり考えていたし、事業への質問も的を得たものでした。 会う前に炎上してしまった経緯も説明してもらっていたのですが、本当に不可抗力だったのだろうなとそこでようやく納得できました。

唯一問題というと彼の今後の専門としたい分野がUXなどであること。当時彼の経験があるAndroidアプリやUnityのポジションは弊社にはありませんでした。 あるのはUXなにそれうまいの?CUIで良くね?と言っちゃう自分しかいないチームでのサーバサイドKotlinでした。 そこで、「すごくHiroくんは優秀だと感じたし、ぜひ働いて欲しいけど、今弊社に来るとHiroくんのキャリア的に遠回りになるかもしれない。本気でおすすめ出来ない。でも同じ界隈にいるだろうし、引き続き仲良くしてね!」と笑顔で送り出すことにしました。

彼が帰った後「すごく良さそうだったけど、今後の彼の専門や今のスキル的にポジションがほぼない。なので今回はなかったことで!」という評価を会社のメンバーに伝えておきました。

数日後

その数日後、TwitterのDMでこんなメッセージが来ました。

「色々週末考えたのですが、Kotinサーバーサイドとして入らせて頂くこと可能でしょうか?」

正直、まじか!空気読めない子か!ってフいたのを覚えてます。(そして今気づいたけど、typoしてるw) でも入りたい!と言ってきたのに、なにもせずにごめんなさいってするのはおかしいので、入社試験をしてもらいました。 一週間くらいかけて貰えばいいかなーって思ったところ、1,2回のラリーはあったものの、2日で作り切り、コードもしっかり読めていて、想像以上でした。 こちら側からお断りする理由もないし、彼のキャリア的に美味しいのか?と罪悪感を感じつつ、入社してもらうことにしました。

ちなみに入社後に彼が書いた下でも紹介している記事を読んで、すごく真剣に話を聞いてもらえたのがわかったし、めっちゃ嬉しかったのを覚えています。

入社前に

今のUbieはまだまだ小さな会社であり、事業を危険に晒すこと=即会社が潰れるということが十分あり得ます。そのため、彼の受け入れ条件としては公開して良い情報・ダメな情報を明確にすることをみんなに約束しました。と言っても、会った時にその話はしっかりすでにしていて、理解していたし、なんなら話す前から理解していたので、全く不安はありませんでした。

入社

一番の問題は彼が他の弊社メンバーに受け入れられず、ぎこちない関係になってしまうかも?という点でした。短い期間しかいないため、早めに打ち解けてもらいたかったです。そこでwelcomeランチがあったのですが、紹介一言目に「あの炎上した子です!」と全体に情報公開しました。彼はめっちゃ焦ってましたが、Ubieメンバーの器のデカさや彼が誠実に説明してるところを見て、みんな安心して受け入れてくれていました。あの早いタイミングでアイスブレイクしっかり出来たのは良かったなーと思います。彼いろんな会社のイベント参加してたし、入社日からUbieの一員になってくれました。

タスク

サーバサイドKotlinのポジションではあったのですが、せっかく来てもらう訳だし、色々吸収してもらおうと考えました。彼には複数のサービスの担当をしてもらい結果的に

  • サーバサイド(Kotlin/Python/Rails)
  • フロント(JS/TypeScript)
  • 実装設計
  • 単独での開発
  • スクラムでのチーム開発
  • デザイン

といろんなことをしてもらいました。全く経験がなかったのに、土日で勉強してきて、フロントの実装も素晴らしい速度でやっていたのは本当に驚きでした。

もちろん彼にもまだまだ未熟な部分はあって、そこは週1の1on1で話していたし、色々彼なりに頑張って改善していました。

結果今の彼のレジュメはぐちゃぐちゃになっただろうと思いますが、master resumeには残して、提出するresumeから外せばいいだけだし、好きにしたら良いと思う(責任放棄)

留学生のインターン

余談ですが、インターンインターンと言ってますが、留学生のインターンって日本で言う、学生の業務委託であり、使えなければクビ切られる覚悟を持っている人が多いです。彼もその考えを持っていました。

もし留学生をインターンとして受け入れる会社があるなら、学ばせるというより、キャリアを積ませる+プロの業務委託的な扱いをしてあげて欲しいです。なので、今回も学ばせると言うより、会社への貢献のためのタスクを選んでやってもらいました。

このあたり読んでもらえると良いのではないかなーと思います。

hiro-ca.hatenablog.com

なぜこの記事を書いたか?

インターン生や新卒などなど色んな人を見てきましたが、こんなブログを書くのは初めてで、今後もあんまり書かないだろうと考えています。しかし優秀な子が不本意なデジタルタトゥーにかわいそうな思いはして欲しくないので、彼はそんな人じゃないということをこの記事で明言しておきます。

彼はフルタイムで働いているため会社の様々な情報にリーチできる人材でしたが、インターン期間中一度も情報公開に関して指導する必要はありませんでしたし、信用できる人間です。

最後に

Hiroくん、お疲れ様でした。色々自分至らぬところがあったと思います。申し訳ないです。でも一緒に働けてすごく楽しかったし、Hiroくんが将来めっちゃ活躍してるところを早くみたいです。そして「わしが育てた」と言わせて下さい。Good luck!!!

*1:弊社の三つのバリューのうちの一つ。100の議論より1の実行に価値がある。

Kotlin Fest 2019に参加してきました

Kotlin Festの感想ブログを読んでいて、やっぱり自分も書きたくなっちゃった。だから書く。(2年連続)

去年の記事

shiraji.hatenablog.com

CfP

今年はCfPだったので、2本書きました。

  • GraphQL周り(通過した話)
  • IntelliJ周り(DroidKaigiでなかなか反応良かった話の改良版)

セッションの内容に新規性・独自性があること Kotlin独自の知見を得られること ― 例えば内容をJavaに置き換えても同じ知見が得られるようなセッションは採用されにくいです。

これらの要件が本当に難しくて、Javaに置き換えられない内容ってCoroutineやKotlinでしか使えないライブラリしかないんでは?と悩みまくりました。それに加えて新規性!?とかなり苦しんだCfPでした。

通過連絡

7/10に通過連絡が来ました。

全然時間なかった!とここで文句書くつもりだったけど、メール確認して7/10に連絡来ていた事実を知り、1.5ヶ月もあったんなら余裕だったんじゃん・・・と今反省しています。いや、夏休みがあったから実質1ヶ月だしやっぱり時間なかった!

スライド準備

CfPの時にアウトラインが決まっていたこともあり、サクサク進むかなーって思ってました。 IntelliJの話がしたくてしたくて、その影響もあり、あんまりモチベーションが上がらず、進捗は良くなかったです。 7月から話す内容を書き出し、2週間前にKeynoteの1枚目を作り出し、1週間前にようやく完成しました。

かなり遅くて焦ったのですが、今までとは別の方法でスライド作成し、その方法が自分には結構フィットしました。これは自分にとってかなり大きな収穫かなーと考えています。

前日

去年同様、ぼっち飯回避ツイートをしました。

kouさん、ikkunさんが反応してくれたので、前日の時点でぼっち飯回避できたの本当に良かった。

当日に追加で呼びかけたらいっせいさんが反応してくれました。これで準備万端になりました!

当日

会場に入ったのは10:40くらいでした(これがあとで叱られる原因に)。もうすごい盛り上がっていて、会場入って、おおお!とすごくワクワクしたのを覚えています。控え室に荷物を置かせてもらい、キーノートを聴きました。 Svetlana Isakovaさんに紹介してもらった、Inline classやimmutableListの話が非常に良く、早速どこに導入するべ?って検討していました。それとSvetlana Isakovaさんのスライドにまさか自分が出てくるとは思ってなかったのですごく嬉しかったです。

最近正直OSSで貢献できてないので、何かしなきゃ!と思いました。

ランチ

前日のツイートで集まった三人の方落ち合おうとしていたら、八木さんがランチ相手を探していたので、八木さんを引っ張っていき、去年と同じところでランチしました。

バックグランドがみんなバラバラだったのですが、去年同様結構盛り上がりました。 話の内容はKotlinの話はもちろん、Androidの話であったり、サーバサイドの話であったり、業務の話であったり、多岐に渡りました。 たった1時間しかなかったのが残念ですが、やっぱり初めての人とご飯行くの楽しいし、知ってる人がいても楽しい!という気づきがありました。 これは来年もKotlin Festがあるならぜひやりたい!

当日(午後)

午後のトークは自分の登壇が15:30からあると言うことであんまり聴いてないのですが、以下の二つを聴講しました。

  • Kotlin コルーチンを 理解しよう 2019
  • 公募によるLT大会

八木さんのトークはコルーチンの知識を最新版にアップデートしようと思いましたが、すんごくうまくスライド作っていて、発表内容も濃く、コードが読みやすく、途中で自分のスライドや発表内容直したくなりました。ちょっと自信を無くして、控え室に行った覚えがあります。

LTはどれも良かったです。途中どうしてもトイレにいきたくなってしまい、漏らして人生終わらすならここで行くしかないと、「静的解析ツール detekt で任意の条件で警告させる」の話途中で抜けてしまいました。lintの話は大好きだったのでめっちゃ悔しかったです。急いでトイレ行った後に戻ってきたら会場が爆笑していて、漏らさなくて良かったけど、トイレはトーク前に行こう!と言う小学生並の知見を得ました。動画に映ってる気がするので、まじで申し訳ないです。すいません。LTはめちゃくちゃよくて、どれも5分じゃなくてもっと長い時間で聴きたい!と感じました。

登壇

スライド

speakerdeck.com

GraphQLの話をする時、どこから話せば良いんだろう?と悩み、結局出来上がったのがKotlinの話をせずに1/3基本的なGraphQLの話をすると言う選択でした。 最初にGraphQLの経験を確認してみたのですが、思った以上に少なくて、この選択が間違ってなかったー!良かったーと話ながら思っていました。 GraphQLのデメリットの話は時間的に削りました。LTでGraphQLのN+1問題が辛いって話が出ていたので、そこで紹介してもらえたし、結果オーライかなーと。

今回意識したのがspeakerdeckではあんまり見えないのですが、スライド間のトランジションです。 GraphQLはデータの流れ、クエリの流れなどを理解することが重要だなと思っており、Magic Moveを駆使してそれを表現してみました。 聴いてくれた方があれがすごく良かったと反応をくれたのが本当に嬉しかったです。

余談ですが、Magic Moveを使う場合、閉じ括弧が消える問題があるのですが、これを解消するために、調べていたら結局Jakeさんに到達して、自分はようやく彼の5年前に追いついたのか・・・愕然としていました。(解消方法は下のツイートのスレッドに記述されていますが、透過率100%の文字を括弧の後ろに入れました。)

オープニングでたろうさんが盛り上げましょうみたいな話をしてくれたので、話を聴いてくれた人たちが結構トーク中に反応してくれて嬉しかったです。 スライド作った当時本当にコードしかなくて、これ絶対寝るわ。って思ったので、どうにかして、興味を失わず、理解してもらう方法を実施していたので、話ていて気持ち良かったです。聴いてくれた人たち本当にありがとうございました!

登壇後

Ask The Speakersで30分の休憩全部使って質問対応しました。 みんなかなり面白い質問をしてくれて自分まだまだGraphQLのこと知らないなーって思いました。

最初APIの開発辛いよねーって大げさな話を出したのですが、まさにあれば起こってるんです!ぜひ導入してみたい!と言う話を何人かにしてもらえたのがやっぱりこう言うのあるんだ。とびっくりしつつ、めっちゃ嬉しかったです。

ブース

ブースですが、朝ブースにいる知っている人たちを見つけて、挨拶をして、あとで来ます!と宣言して、Ask The Speakers後に向かいました。 しかし、自分のAsk The Speakersが終わる時間がブースの撤収開始時間らしく、ほぼほぼ回れませんでした。。。 家族がブースでもらえるノベルティが大好きで、Twitterを眺めていて、あれ良さそう!と自分の帰りをワクワクして待っていたようなのですが、ステッカーだけもらって帰って来た自分に対し、なぜもっと早く会場に行かなかったのか?とガチ目な説教をしてきました。次回以降しっかり早めに行きます。。。

各社のノベルティ、すごく工夫されていて -> 持って帰る -> 家族が喜ぶ -> その会社のプロダクトを使う と言う良い流れが私の家では出来ています。

懇親会

懇親会ですが、結構いろんな方と話せてすごく楽しかったです。 Svetlana Isakovaさんに登壇直前の時間にあとで話しかけるね!と宣言しており、immutableListの話を聴けて満足しました。

また初めての方が結構な数話しかけてくれたのが本当に嬉しかったです。自分が知ってる人に対しては話しかけることができるのですが、初めての方は紹介してもらうなりしないと遭遇出来ないです。 でも食事を盛っている時とか、「今話しかけても良いですか?」とか言ってもらえて本当に嬉しかったです。 ご飯食いたいけど、それ以上に話したいってのがあるので、どんどん話しかけてもらえるとありがたいなーと。 お子さんを連れて参加していたポールさんとそのお子さんとお話も出来たのも良かったです。自分もいつか子供連れて勉強会とか参加したいなーと。

まとめ

来年もあったら絶対参加したい!絶対また登壇したい!おー!!!


一人で馴染みの店で打ち上げ。めっちゃ美味かったー。

Yet another emoji support をリリースしました。

Yet another emoji supportという絵文字の入力をサポートするIntelliJプラグインをリリースしました。

f:id:shiraji:20190607004731g:plain
イメージとしてはこんな感じ

f:id:shiraji:20190607004809p:plain
Commitダイアログでも利用できます。

github.com

経緯

元々Emoji supportプラグインを公開していました。しかし、いくつかのissueで絵文字をコミットダイアログ以外のところでも挿入したいと言う要望を受けることが多くなりました。cmd+ctrl+spaceで入力できると何度も伝えていたのですが、なぜかみんなあんまり納得してくれませんでした。リリースから3年経過し、公式のサポートもまだないため、需要があるかさっぱりわからないですが、一旦作ってみるかということで作りました。

特徴

特徴としてはどの言語でもコメントでの入力をサポートしています。また自分の気づいたベースの言語に関してはStringの中での入力ができるようになっています。(GroovyとScalaが初期リリースでサポートされていないのは完全に失念していたためです。次のバージョンで対応予定です)

細かい点としてはKotlinのようにString内でコードを書ける場合、コードを書くことが可能な箇所ではCompletionが出ないようになっています。

val foo1 = "${:<caret>}" // この場合は出ない
val foo2 = ":<caret>${}" // この場合は出る

graphql-spring-boot-starterのExceptionハンドリングがめっちゃ便利になってた

Kotlinサーバサイド開発者のみなさん、graphql-javaでの開発の調子はどうでしょうか?

去年末にリリースされたバージョンのgraphql-spring-boot-starterのExceptionハンドリングがめっちゃ便利で感動したので、ブログに残しておきます。

想定読者

  • graphql-spring-boot-starter使って、GraphQLやってる人/これからやる人

はじめに

graphql-spring-boot-starterのバージョンが5.0.4の時、Resolver内で発生したExceptionは特に何も指定しない場合、全て Internal Server Error(s) while executing query と言うmessageを返していました。 例えば、AuthException のような自前で作成したExceptionが出た場合、レスポンスとしては違ったものを返したくなりますが、問答無用でInternal Server Error扱いです。(ちなみに最新版でも互換性を残すため、同じ挙動です)

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw IllegalArgumentException("icd must not be empty")
        return service.getDiseases(icd)
    }

f:id:shiraji:20190424022910p:plain
何だろうとInternal Server Error!

そこで、カスタマイズしたエラーを返すようにしたいのですが、探せど探せど良い方法が見つかりません。issueにこのドキュメント通りに書けばいけるぜ!と書かれているので、URLをクリックすると

        \          SORRY            /
         \                         /
          \    This page does     /
           ]   not exist yet.    [    ,'|
           ]                     [   /  |
           ]___               ___[ ,'   |
           ]  ]\             /[  [ |:   |
           ]  ] \           / [  [ |:   |
           ]  ]  ]         [  [  [ |:   |
           ]  ]  ]__     __[  [  [ |:   |
           ]  ]  ] ]\ _ /[ [  [  [ |:   |
           ]  ]  ] ] (#) [ [  [  [ :===='
           ]  ]  ]_].nHn.[_[  [  [
           ]  ]  ]  HHHHH. [  [  [
           ]  ] /   `HH("N  \ [  [
           ]__]/     HHH  "  \[__[
           ]         NNN         [
           ]         N/"         [
           ]         N H         [
          /          N            \
         /           q,            \
        /                           \

こんなページに飛ばされ、かなりイライラします。*1

でまぁ、色々調べていくと、Authエラーなどで全処理中断させたいであれば、 GraphQLException を実装すれば、一般的なランタイム時のExceptionとなり、エラーを返してくれると書いてあるので、試してみます。

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw GraphQLException("icd must not be empty")
        return service.getDiseases(icd)
    }

結果

f:id:shiraji:20190424023144p:plain
返してくれなかったよ・・・涙

このあたりで心折れます。

Spring BootでのExceptionハンドリング

話変わって、Spring Bootには結構便利なExceptionハンドラがあります。 以下のような感じで、各種Controller内で発生したExceptionに対して、一箇所でうまくシュッとやってくれます 😤

@RestControllerAdvice(basePackageClasses = [ExceptionHandlerAdvice::class])
class MyExceptionHandlerAdvice {

    @ExceptionHandler(TimeoutException::class)
    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
    fun handle(exception: TimeoutException): ExceptionResource {
        log.error("caught an exception", exception)
        return ExceptionResource("タイムアウト")
    }

    @ExceptionHandler(WebExchangeBindException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handle(exception: TimeoutException): ExceptionResource {
        log.error("caught an exception", exception)
        return ExceptionResource("なんか起こった")
    }

}

まぁあんまり詳しく書いても話が逸れるので、こんな感じのものがあるのだなー程度で良いです。

で、graphql-javaでもこんな感じで処理したい!と思う訳です。

時間が解決してくれた(=中の人達が頑張ってくれた)

それから数ヶ月が経ち、graphql-spring-boot-starterがメキメキ更新されていました。

現在の最新のバージョン(5.7.3)を使い、graphql.servlet.exception-handlers-enabledをtrueにすると以下のコードでもうまくハンドリングしてくれるようになります。*2

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw IllegalArgumentException("icd must not be empty")
        return service.getDiseases(icd)
    }

f:id:shiraji:20190424030032p:plain
IllegalArgumentExceptionですらうまくメッセージを返すように

ものすごく良くなりました。しかし一点typeがException名になってしまいます。 フロント側で IllegalArgumentException を処理してもらうで良いのですが、ちょっとダサい。GraphQLExceptionをthrowしたとしても、同じようにtypeが表示されてしまいます。

Spring BootのExceptionハンドリングのように特定のExceptionに対して、カスタマイズしたGraphQLErrorを表示したくなります。

そこで登場したのが、exception-handlers-enabledが導入されたタイミングでgraphql-spring-boot-starterが @ExceptionHandler によるExceptionのカスタマイズ機能です。実装方法としてはこんな感じのBeanやComponentをクラスを作るだけ!

@Component
class GraphQLExceptionHandler {
    @ExceptionHandler(IllegalArgumentException::class)
    fun handleSomeException(e: Throwable): GraphQLError {
        return GenericGraphQLError("Foo! ${e.message}")
    }
}

それで同じようにExceptionを発生させると

f:id:shiraji:20190424031759p:plain
メッセージやtypeを表示しないなどのカスタマイズが出来た!

これにより、エラーメッセージのカスタマイズやtypeなどを表示しないなどなどエラーハンドリングが容易に出来るようになりました。

実際のコード

実際にUbieで公開している https://github.com/ubie-inc/kotlin-graphql-sample に、動くコードをコミットしてあります。上記には記述してありませんが、graphql-java-toolsのバージョン上げも必要になりますので、ご注意してください。

github.com

*1:https://graphql-java.readthedocs.io/en/latest/https://www.graphql-java.com/documentation にリダイレクト機能なし+ドキュメント削除して移動したことが原因です

*2:5.3で導入され、5.4.1でバグfixされています。

Androidエンジニアからベンチャー企業でサーバーサイドエンジニアに転向して半年経って分かったこと

8/20入社だったので2/20でUbieに入社して半年になりました。つまりサーバサイドエンジニアに転向して半年経ちました。

以前の記事でこんなこと書いたので、続編を書いてみようと思います。

半年くらい働けば、きっと悪いところももっと見えてくるはずなので、来年落ち着いたらまた続編を書いていきたいと思います。

Ubie, incに入社しました。 - shiraji’s diary

今月のツイートを冷静に眺めてみるとそこまで自社に関してのツイートしていないし、恋は盲目状態から解放されているようなので、冷静に自分のことを分析してみようと思います。

サーバーサイドにキャリアチェンジしてどうだったか?

サーバサイドにキャリアチェンジしたのが正解だったかどうかに関してはまだわかっていません。これに関しては結論が当分出ないような気がしています。ただ、以前のエントリーにも書いてありますが、元々AndroidアプリもUIのことより通信だったり、パフォーマンスだったりを主に見ていたため、AOSPの辛さがなくのびのびKotlinを書けるという環境は本当に楽しいです。今後、多くのネイティブアプリエンジニア達がサーバサイドに目を向けるようになってきた時にサーバサイドKotlinというのは一つの可能性でもあるし、ガンガン発信していって、サーバサイドKotlinのプレゼンス高めていきたいという想いがあります。

新しいチャレンジ

新しいチャレンジとしてはGraphQLに手を出し始めたのは自分にとって大きな出来事でした。

shiraji.hatenablog.com

shiraji.hatenablog.com

GraphQLといえばGo、Rubyなどといった言語を使うのが主流な気がしますが、JVM言語でも可能性あるのでは?というところまで自分の中で良い印象を持っています。さらにここにKotlinならではの良さを追加出来ないか?と模索しているところです。

Androidアプリ開発と比較しての設計

設計に関しては、Androidアプリのように活発な動きはなく、特にAPIの作成の時などはシンプルな構成になります。ただし、Androidアプリでの経験が活きないか?と言われるとそれは間違っていて、Androidアプリで利用したDIやクリーンアーキテクチャなどは元々サーバサイドにあったものですし、そのまま利用することが出来ています。

開発環境

今まではAndroid Studioを使っていましたが、IntelliJ IDEAに乗り換えています。Android StudioIntelliJを両方使ったことある人であれば、わかると思いますが、特に使い勝手は変わらず、多くのサーバサイドKotlinサポート機能が乗っていて、そこの乗り換えコストはほぼ0です。(自分はIntelliJ IDEAでプラグインを開発していたという経験もあるので、生存バイアスかかってるかもしれませんが。。。)

さらにCoroutineやテストツール(JUnit5やmockkなど)など馴染みがあるツールをそのまま利用できる事もあり、サーバサイド辛いなーと感じることが少ないです。

サーバサイドエンジニアとしての課題

自分が今課題と感じているのが、自分自身、結構ネイティブアプリの開発に注力しすぎてしまったため、ネイティブアプリのライブラリなどの知識は深くなっていますが、昨今のウェブの開発スタイルにまだ馴染めていません。UbieではGCPを主に使っていますが、GCPにどんな機能があるのかを完全に把握しておらず、実装しようとして、あれ?その機能GCPにあるのでは?というツッコミを受けたことがあります。その機能がどういうものなのか?などの把握はそこまで時間がかからないのですが、自分の引き出しの少なさにちょっとがっかりしています。

サーバサイドの進化はネイティブ開発に比べ遅い!と勝手に思って7,8年前までのサーバサイド開発のスタンスでいたのですが、これは完全に間違いでした。

Androidアプリエンジニアとしての不安

Androidアプリ開発の一線から退いて半年経ち、一番の不安は、最新のライブラリのキャッチアップが出来ないことです。例えば、JetPackの存在は知っていますし、コードはある程度眺めています。しかし、仕事でJetPackを使っていないですし、経験がありません。今年のGoogle IOでまた新しいものが発表された時、それに対して何かしら自分がアクションを起こせるかどうかは正直不明です。そのため、もしAndroidアプリエンジニアとしてカムバックしなくてはならない時、何かしらキャッチアップ期間がないと厳しそうだなーという感じています。

実際、今サーバサイドエンジニアにカムバックしたといえばカムバックした訳で、このキャッチアップに時間を使っているというのも不安です。半年経った今でもまだサーバサイドエンジニアですと胸張って言えないような感じなので、キャッチアップって結構時間かかるなーという印象を持っています。

ベンチャー企業に入ってどうだったか?

今までいくつかの企業で働いていて、上場してない企業としてはUbieは2社目です。それ以外の会社は全て上場していて、多くのユーザーを抱えている企業でした。1社目は10人くらいで、今回は現在20人くらいなのでほぼ変わらない規模です。こういう会社に入ると感じるのは自分の稼働が会社へダイレクトに良くも悪くも影響を与える事です。変なバグを出してしまったりすれば会社は潰れるかもしれませんし、逆に素晴らしい機能を出せば一気に会社が大きくなる可能性があります。自分が会社を動かしているかも?という錯覚すら覚えて喜べます。1社目で感じていたのですが、こういう感覚が好きなエンジニアには小さい会社ありだなーと改めて思いました。

f:id:shiraji:20181220164930j:plain
アイキャッチ画像がなかったから、無駄に、自分のデスクのマスコットのアー写置いておく。

ルール

新しい会社にはよくあることですが、ルールが決まっていないかったり、そもそもなかったりします。必要であれば社員みんなで議論して、即ルールを制定しています。

こういうことは小さな会社だから出来る自由さ、フットワークの軽さがあって、すごくいいなーと感じています。

働き方

働き方は今までよりワークライフバランスを特に意識するようになりました。これはリモートワークがいつでも自由に出来る状況*1であることが起因しているのですが、やろうと思えば、深夜でも早朝でも土日でもプライベートの時間を削って仕事が出来る状況になっています。 自分のプライベートのGitHubアカウントを会社でも使っているので、GitHubの通知を見ようとすると会社の通知とOSSの通知を同時に受け取ってしまえます。さらに言うと、自分の会社の出社時間が10:00-17:30でだいたい多くの人より早く帰っており、家で足りない稼働分(30分)働いています。そのためそのまま流れで気づいたら仕事してたみたいな状況に陥りがちです。会社の今のフェーズはできるだけ多くのリソースを割いて、サービスをどんどん良くしていくというのはわかっています。しかし今まだ設立1年半のこのタイミングでエンジニアがいっぱい残業してしまうと今後入ってくる人もそういうのを会社は期待してしまうし、それは健全ではないと思っているので、自分1人だけでもワークライフバランスを特に意識しています。

タスク

任されているタスクは結構自分から手を挙げたものが多いです。

  • サーバサイドKotlinの実装
  • DBテーブルの設計・実装
  • 協力会社との連携処理の設計・実装
  • SREの細かいタスクのサポート
  • bot作成(これはほぼ遊びか?)

今フロントエンジニアの質がものすごく充実しているように見えます。結構勉強出来そうな環境です。しかし、今の自分がフロントで足引っ張るより、サーバサイドや人が足りていないSREで足引っ張った方が会社のためになるでしょという判断をしたため、SREエンジニアの修行させてもらっています。手取り足取り @sakajunquality から知見吸えているのですごく良い判断したなーって思っています。

時間の使い方

これまでの会社と一番違う部分は実は時間の使い方なんじゃないかと思っています。今までの会社ではリーダーだったり、外部の方とのやりとりだったり、雑多な管理だったり、採用だったりと、そこまでまとまったの時間を開発に割いていませんでした。(これだけ読むとダメじゃんー!って思われるかもしれませんが、自分はこれはこれで実は結構好きだし、楽しかったです。)

今の会社では毎週以下のような時間割でやっています。

  • 会議(3時間15分 *2 )
  • その他、定期的ではない会議や採用周り、週平均2時間くらい
  • 残り週30時間以上は開発

この定期的な会議の多くは出社推奨日の水曜にまとめています。そのため、結構まとまった時間開発することが出来ています。ただ、これでも自分の会社の滞在時間の制限があるため、週1以上は必ずリモートで仕事をし、より長く開発に時間を割けるように工夫しています。

初めての経験

Ubieに入って初めての経験がいくつかありました。

OKR/KPT

スクラムなどは経験あったのですが、今まで業務でOKRやKPTを実際にやったことがありませんでした。OKRは今期から本格的に実施することになり、会社から本を買ってもらい、みんなで勉強して実施するようになりました。KPTは良かったことを褒め合う機会と問題の共有・対処の検討などをしっかりやれるのが気に入ってます。

アウトプットする人が多い

今まで自分より登壇する回数が多い同僚と働いたことがなかったです。Ubieでは社外へのアウトプットを推奨しており、エンジニアはもちろんのこと、デザイナーや医師、Bizもアウトプットする機会があればどんどんアウトプットしています。これがすごく刺激的で、自分ももっとアウトプットしたい!という気持ちが湧いてきます。

名前

これは入社時からだけど、会社で初めて「しらじ」って呼ばれてる。呼ばれすぎてて、社外の方と話すときに、自分のことを本名で呼ばれたりするとドキっとするようになってしまった。。。

引っ越し

先日Ubieは引っ越しをしました。新しい場所は三越前駅徒歩10秒です。

goo.gl

今までも席替えやビルの部屋が変わるなどは毎年1,2回のペースで経験してましたが、オフィスの場所が変わる引っ越しは初めての経験でした。新しい環境で働くのはいつもワクワクなので、引っ越してもそこまで変わらないだろうなーって思ったのですが、引っ越して駅から近くなって、オフィスも広くなって、会社の成長をモロに実感出来たのがすごく嬉しかったです。ベンチャー企業の醍醐味だなーこれ。と感じています。このオフィスも2年以内に引っ越す予定なので、次はもっと大きな場所で働けるようにがんばろー!って気分になれました。

最後に

Androidエンジニアからのキャリアチェンジですが、SREにも手を出してますし、今のところうまく進んでいる気がします。会社の成長を感じられるのが本当に嬉しい日々です。

さて、こんなUbieですが、引き続き、いろんな職種を募集しています。

speakerdeck.com

随時オフィス見学やUbie Dayなど社外の方を呼んで来てもらうようなイベントもやっていますので、興味あれば、TwitterでメンションやDM下さい。

*1:最大週2で、出社推奨日もあります

*2:1時間チーム内KPT, 30分全体KPT, 30分採用定例, 1時間エンジニア定例, 15分業務委託の方と1on1