Lobi で Android 4.1 をサポート終了した話

こんにちは!

ゲームコミュニティ事業部(Lobi)サービス基盤チームでアプリエンジニアをしているジェイソンです。

僕が開発に携わっている ゲーム攻略チャットSNS、マルチ掲示板 Lobi(ロビー) は、 その前身であるナカマップのリリースが2010年12月15日なので、つい先日運用8年目を迎えたことになります。

現在、ストアに公開されている最新バージョン、Lobi Android v12.0.0では、最低動作環境を以下のように変更しました。

  • (旧)Android 4.1 以降動作
  • (新)Android 4.2 以降動作

本記事では、最低動作環境をなぜ上げることにしたのか、上げることでどのようなメリット・デメリットがあったのかをご紹介していきます。

旧バージョンのOSのサポート方針

Android 4.1~4.1.1(Jelly Bean)は、2012/07/09に公開されてから、すでに5年以上が経過しているバージョンです。

Project Butterによる動作高速化や、Smart App Updateによるアプリの差分アップデートなど、当時はとても興奮したOSバージョンでした。

今でもこのバージョンを使っている方が一定数いるため、可能な限りサポート対象として、ストアからインストールできるようにしてあげたいところです。

しかし、Android 4.1.x をサポートし続けることによって、正確にはサポートするOSバージョンが増えることによって、 どのOSバージョンのOSでもアプリを正常動作させるため、テストや保守コストが増大してしまいます。 また、サポートライブラリでカバーしていない新しいAPIを使えないなどの理由で、バックポートライブラリの導入を検討したり、要件の見直しが必要となるケースが発生してしまいます。

そのためLobiでは、対象OSバージョンのDAUや新規流入がある水準を下回ったら、サポート終了として検討を進める運用をしています。 実際にサポート終了するのは、進行中の施策状況や、リリース計画に影響がでないタイミングとなります。

WebPのフルサポート

Lobi の Android/iOS アプリは、昨年末のバージョンからWebP形式の画像をサーバーから取得するようになりました。 これは過去にもご紹介しています。

Lobiで画像のWebp変換による通信量削減と調査のためにAWS Athenaを利用した話

しかし、Android 4.1.x 環境に限っては、従来通りJpeg形式の画像をサーバーから取得していました…

なぜなら、WebP形式の画像自体は Android 4.0 以降であればサポートしているものの、 当初は非可逆圧縮かつ、透明度が不要な画像でしか使えなかったのでした。 (Android 4.2.1 以降であれば、可逆圧縮・透明度ありなWebP形式をサポートしている)

Supported Media Formats

Android 4.1 サポート終了にともない、上記の分岐が不要となったため、画像取得周りがシンプルになりました。

また、アプリで扱う画像形式が統一されることで、キャッシュ効率の改善も期待できそうです。

Activity#isDestroyed

Activity#onDestroy の実行有無を判定するために、Activiyt#isDestroyed を使いたいことがありました。

しかしこのメソッドは Android 4.2 以降でないと使えず、いままでは以下のような分岐が必要でした。

if (DeviceUtil.hasJellyBeanMR1()) {      
    return !activity.isDestroyed();       
} else {      
    return true;     
}

Android 4.1 サポート終了にともない、以下のようにシンプルに書けるようになりました。

public static boolean checkActivityRunning(Activity activity) {
    if (activity == null) {
        return false;
    } else if (activity.isFinishing()) {
        return false;
    } else if (activity.isDestroyed()) { ← シンプル!
        return false;
    }

    return true;
}

UserAgentの取得

UserAgentを取得したいとき、Android 4.1 以前は以下のようにしなければいけませんでした。

new WebView(context).getSettings().getUserAgentString();

しかし Android 4.2 以降では、UserAgent取得用のメソッドが用意され、以下のようにシンプルに取得できるようになっています。 WebSettings

WebSettings.getDefaultUserAgent(Context);

Android 4.1 のサポート終了によって、不要となった分岐処理をまとめてみましたが、こう見るとそれほど多くはありませんね。

これはたまたま、Lobi Android に影響を与える変更が少なかったということもありますが、 Android Support Library を積極的に使っていることも一因かもしれません。

みんな大好き Android Support library

Android Support Library として、さまざまなライブラリが公開されており、それぞれが後方互換性や追加の機能を提供してくれます。

また、特定のOSで発生するバグなどが修正されている場合もあります。

Lobi Android のように、できる限り幅広いOSバージョンをサポートしたいアプリの場合、Android Support Library は非常に有用です。

Activity であれば ActivityCompat、ImageView であれば ImageViewCompat のように、 Android が提供しているクラスを使うときには *Compat が用意されていないかどうかチェックして、積極的に使っていきましょう。

最低動作環境を上げることによるデメリット

Android 4.1 環境の端末を使い続けている方は、今後Lobiアプリの新規インストールや、バージョンアップができなくなってしまいます。

そのため、対象となる端末でインストールできる最新バージョンで大きな不具合が発生している場合は、リリーススケジュールの調製をするなど、状況に応じた対応が求められます。

また移行期間として、最新バージョンのアプリをインストールできない人向けに、旧バージョンをインストール可能にしてあげるなど、Android 4.1 環境の端末を使い続けている方ができるだけ不快な思いをしないようにしています。

まとめ

最後は Android Support Library の紹介となってしまいましたが、Android 4.1 をサポート終了することによって嬉しいポイントをご紹介しました。 長期間にわたってサービスを継続させるためにも、旧OSバージョンのサポート方針について明確にすることはとても大切ですね。

旧OSバージョンのサポート終了について悩んでいるエンジニアの皆さまの一助になれば幸いです。

カヤックではサービス持続性を大切にしたいエンジニアを募集しています

【決定版】中級猫でもわかる正規表現再入門

正規表現

初めまして!技術部サーバーチームのダリエンと申します。正規表現は役に立つ知識なので、勉強してみました。
では、正規表現について調べた知識をシェアしたいと思います。

こちらは Tech Kayac Advent Calendar 2017 の25日目の記事になります。

イントロダクション

定義

  • 検索パターンを象る文字列
  • 文字列の集合を一つの文字列で表現する(ja.wikipedia.org)
  • 英語:Regular Expression(s) / Regex

    何のために使いますか?

  • ウェブブラウザーエクステンションやIDEやコードなどで:
    • 文字をそのまま検索 (⌘/Ctrl+Fと全く同じ)
    • パターンの一致

      ⌘/Ctrl+Fは簡単でしょう?

      そうです。しかし、パータン一致は? 下のテキストを検討してください:

JPY152
JPY40501
USD501
IDR1261
JPY999

上のテキストで、3桁JPYしか絞り込みたくない時、⌘/Ctrl+Fでできませんね。
正規表現の(?<=JPY)\d{3}$で、152と999を取得することができます。

さらに、プログラムの中に⌘/Ctrl+Fを実行することができませんね。正規表現ならできます。

結局、正規表現を使えるようになるためにはこのようなパターンがわからないといけない: ^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$ 難しそうですが、できたら本当に役に立ちますから、勉強甲斐があると思います!
例えば、上のパターンで、どんな浮動小数点数でも検索できます。便利ですね。 なので、正規表現を勉強しましょう!

量指定子

定義

正規表現と文字列が合致しないといけないという個数。

種類

  • {N,N} : 範囲の量指定子 (range quantifier)
    例:a{2,4} ~ aaとかaaaとかaaaaとか
  • * : 0以上の量指定子
    例:(ab)c* ~ ab, abc, abcc, abccc, …
  • + :1以上の量指定子
    例:(ab)c+ ~ abc, abcc, abccc, …
  • ? : 0または1の量指定子
    例:(ab)c? ~ abまたはabc

    注意:量指定子は、量指定子の演算子の左の「文字一個・グループ一つ」だけに影響があります。

量指定子の大切な概念:最長一致

一般的に、量指定子は最長一致という挙動を持っています。それで、できるだけ一番多い結果を戻してみます。
例:abcccccc は、abc* で合致されると、abccccccを戻します。(ab,abc, abcc, abccc, … じゃありません)。
この案件に、*と言う量指定子は 0 から6まで ’c’ を戻せるんですが、最長一致に処理しますから、一番多い結果(6)を戻します。

最短一致

できるだけ一番少ない結果を戻してみます。
例:abcccccc は、abc*? で合致されると、abを戻します。(abc, abcc, abccc, …, abcccccc じゃありません)。
この案件に、*?と言う量指定子は 0 から6まで ’c’ を戻せるんですが、ものぐさ的に処理しますから、一番少ない結果(0)を戻します。

最短一致を合致しみる量指定子シンタックス:
{パターン量指定子}? 例:abc*?abc+?abc??abc{2,5}?

グループ

後方参照グループ (Capture Group)

括弧に入れられる正規表現は後方参照グループと言われます。

後方参照グループは二つのことに使います: - 綺麗な正規表現を書けるように(色々なパターンがグループに分けられた正規表現は見やすい) - backreferences(後方参照)で呼ばれるようになります。

例:(Tarzan*)がある場合は、\1Tarzan*という意味があります。 解説:(Tarzan*)は後方参照グループ、\1 は後方参照です。 (Tarzan*)は第一のグループなので、そのグループの参照のために、\1で書きます。

後方参照 (Backreference)

後方参照グループを参照するため正規表現です。
シンタックス:\{集合グループ数}\1, \2, \3, …)
例:HelloBelleHelloBelleImGaston は、適当な正規表現で表現:(Hello)(Belle)\1\2ImGastonです。
解説:Helloは第一の集合グループ、Belleは第二の集合グループなので、\1Helloを参照して、\2Belleを参照します。

名前をつけるグループ (Named Capturing Group)

名前をつけるグループは数で参照しなくて、名前で参照します。それで、グループは名前をつけなければなりません。
RubyやPerl (SublimeTextの正規表現エンジン)でこのシンタックスを使うことができます: グループ:(?<名前>パターン)
参照:\k<名前>
使い方は普通の集合グループと全く同じです。

非後方参照グループ (Non-Capturing Group)

普通のグループですが、参照されることができないというグループ。
例:(?:Tarzan*)はどこにいるか\1 → エラー
解説:(?:Tarzan*)は後方参照グループじゃないので、第一集合グループはまだ存在していません。それで、\1は存在していない 後方参照グループを参照してみますから、エラーになります。
例:(?:Tarzan*)(Belle)がある場合は、どこかに\1があったら、その\1Belleを参照します。
解説:(?:Tarzan*)は 非後方参照グループなので、(Belle)は第一 後方参照グループです。 

アトミックグループの導入:バックトラッキングの概念 (Backtracking)

正規表現は、パターンを合致する処理にバックトラキンッグという処理を使います。
バックトラッキングとは:失敗された合致する処理の対応のため挙動です。

量指定子と関係がある大切な概念。

量指定子があるregexが失敗した時、その量指定子で合致した文字は一個除いて、全ての合致する処理を繰り返します。

最短一致量指定子の場合は、一個除かなくて、一個追加します。

例:abccddab(.+)ddで検索されるとします。

  1. regexのabは文字列のabを合致しました。
  2. regexの.は最長一致にccddを合致しました。
  3. 全ての文字列の文字が合致されたので、regexに残るddはなにも合致できないので、regexは失敗でした。
  4. バックトラック:.は全ての残っている文字を合致するのはなくて、一個少ない文字を一致します(ccddからccdになります)。
(1)&(2)を繰り返した後、文字列にまだ合致しいなかった文字は:d
  5. regexに残るddは文字列の残っている文字を合致しみて、regexの第一のdを合致したけど、最後のdがまだ合致しませんでした。まだ失敗でした。
  6. バックトラック:. はもう一回一個少ない文字を一致します(ccdからccになった)
(1)&(2)を繰り返した後:文字列にまだ合致しいない文字は:dd
  7. regexに残るddは文字列の残っている文字を合致しみます。
今回、全ての文字列に残っている文字(dd)が合致したので、regexは成功でした(abccddを返します)。

独占的量指定子 (Possessive Quantifier)

バックトラッキングじゃない量指定子です。
シンタックス:{パターン量指定子}+ー 例:abc*+abc++abc?+abc{2,5}+

アトミックグループ (Atomic Group)

普通な後方参照グループと同様ですが、 Atomicグループを使うと、失敗する時にBacktrackをしません。つまり、合致する処理の時にグループが固めます。
シンタックス:(?>{パータン}) 例:
バックトラッキングの例の復習:abccddは正規表現で検索されるとしますが、 ab(.+)ddで検索される代わりに、ab(>.+)ddで検索されるとします。
1. regexのabは文字列のabを合致しました。
2. regexの.は最長一致にccddを合致しました。
3. 全ての文字列の文字が合致されたので、regexに残るddはなにも合致できないので、regexは失敗でした。
4. (>.+)はアトミックグループなので、正規表現の.の部分がバックトラッキングされません。ですから、合致処理は完了です。regexは失敗でした。

条件 (Conditional)

If...elseのような挙動を持っている正規表現です。
シンタックス:(?(A)B|C)
基本的には、Aは後方参照。Aが参照されるグループが合致した場合は、Bに続きます。合致しなかった場合は、Cに続きます。
例:

FOObar
FOObaz
foobaz

は、(FOO)?(?(1)bar|baz)で検索される場合は、戻した文字列は:FOObarbazです。
解説:
第一の文字列 (FOObar) の合致する処理:FOOが合致したので、barに続けました。それで、FOObarを戻します。
第一の文字列 (FOObaz) の合致する処理:FOOが合致したので、bazに続くのは間違えます。それで、なにも戻しませんでした。
第一の文字列 (foobaz) の合致する処理:FOOは合致しなかったので、bazに続けました。それで、bazを戻します。

オア (OR)

またはのロジック。(a|b)の意味は、aとかbとかもOKです。だけど、両方abのはダメです。
例:

gray
grey
graey

は:gr(a|e)yで合致する場合は:graygreyを戻します。

特別な文字

下述文字は特別な意味を持っています。ですから、regexに入れると文字通りに扱われません。
\d : 一桁の数字
\w : 文字(アルファベット、数字、Underscore)
\s : ホワイトスペース (スペース、横/縦タブ、新しいライン、キャリアッジ・リターン、等)
. (dot) : \n以外、任意の文字
\t : 横タブ
\h : スペース
\v : 縦タブ
\r : キャリアッジ・リターン
\n : 新しいライン(Windows以外、キャリアッジ・リターンも含めています)
普通な . (dot)を使いたい時、エスケープ(\.)が必須です。
\d, \w, \sの否定:\D, \W, \S
例:
\Dの意味は数字以外(アルファベットとか、ホワイトスペースとか)
\Wの意味はアルファベット以外(ホワイトスペースとか)

文字クラス (Character Class)

定義

いくつかある文字から一つ文字しか合わせない条件を作りたい時、文字クラスを使います。
シンタックス:[文字]
例:gr[ae]ygraygreyが戻せます(graeyは戻さない)

メタ文字 (Meta Character)

文字クラスの中では、], \, ^, - 以外は文字通りに扱われます。それで、後方参照グループとか量指定子とかは意味がなくなります。 [ , ], -, \ とかはメタ文字と呼ばれます。これらをクラスに入れたい時、エスケープしないといけません(\[, \], \-, \\ になります)。

メタ文字:^(キャレット ー Caret)

^ は、[]の中に意味が切り替えます。[^{文字}]の意味は:文字の否定。
例:
gr[^ae]ygraygreyが戻せない(aとeが断れたので)。ですが、他の文字が戻せるようになります。grby, grcy, …
^ は文字通りに扱われたい時に、後ろに置かないといけません。例:gr[ae^]y

メタ文字:-(ハイフン ー Hyphen)

-は範囲の意味を持っています。
例:[A-Za-z0-9]の意味は A から Z まで、 a から z まで、0 から 9までです。それで、abcdefghijklmnopqrstuvwxyz0123456789を書くのは必要じゃなくなります。  

ハイフンで文字クラスの減算も実装できます。
文字クラスの減算:[{クラス}-[{減算}]]
例:[a-z-[aiueo]] = [b-df-hj-np-tv-z] = 子音を戻します。

量指定子で文字クラスを繰り返す

[…]+は、合致した文字を繰り返さなくて、すべての字クラスを繰り返す。それで、[0-9]+は繰り返す数しか表示されなくて、なんでも数字が表示されます。

  • 解決は:([…])\1+
  • 例:
    [0-9]{5}の意味は:[0-9][0-9][0-9][0-9][0-9]
    ([0-9])\1{4}の意味は:
    もし、[0-9]の結果は5だったら、regexは(5)\1{4}になりますから、regexの意味は55555です。
  • 解説:文字クラスの結果はグループに入れて、グループの中は \1で参照されて、最後はその後方参照が+ で繰り返します。

アンカーと境界

アンカー:定義

regexエンジンの位置をアサートするのに使います・regexのポインタの現在の位置はどこにあるかをチェックします。
アンカーでチェックできる位置: - 文字列の冒頭:^, \A - 文字列の終了:$, \z

さらに、(?m)という修飾語がある場合は、^$の意味の変更があります: - ラインの冒頭:^ - ラインの終了:$

アンカーの種類

  • ^(キャレット):文字列の冒頭アンカー (?m)を使うと、ライン(文字列じゃない)の冒頭になる
  • $ :文字列の終わりアンカー *(?m)を使うと、ライン(文字列じゃない)の終了になる
  • \A:文字列の開始。 ^との違い:\A(?m)に、(?m)を使うと意味が変えられない(^が変わります)
    例: Apple\nApricot\AAを使うとAppleしか戻しません(Apricotは新しい文字列じゃない)
    (?m)^A を使うとAppleApricotを戻す(Apricotは別のラインにあるので)  
  • \z$と同様ですがラインの終わりじゃなくて、文字列の終わり(\Aと^の関係と同じ)  

境界

\b:ワード境界
\bの前とか後とか、ASCII/Unicode文字とか数字とかunderscoreとかあるかどうかをチェックします - 合致する条件は文字がありません。
\bの否定:\Bです。
例:
\bcat\b

  • bobcatを合致する場合:なし(catの前bがあります)
  • catfishを合致する場合:なし(catの後fがあります)
  • his cat eatsを合致する場合:catを戻します(catの前と後は文字じゃなくて、ホワイトスペースです)

修飾語

定義・種類

Regexの挙動を変えるのに使います。

種類

  • (?i) - 大文字と小文字の区別じゃないのに使います
  • (?s) - DOTALLモード (.\r\nも合致します)
  • (?m) - 複数行モード (^$の挙動の変更に使います)
  • (?x) - FreeSpacingモード (Regexを読みやすいために、regexを書く時スペースと新しいラインが使えます)

    修飾語の書き方

  • 複数修飾語は一緒に含めていることができます。
    例:(?ismx)

  • どこにも書けます。修飾語の効果は修飾語の後に開始です。
    例:

caseSensitive(?ix)whOcAreSaBoutCases
asLonG
asImAlive

説明: (?ix)の前に、文字大小はチェックして、すべてのregexは同じラインにかかないといけないんですが、(?ix)の後には文字大小を無視して、複数行regexも書けます。

  • 修飾語の効果を制止したい時、(?-{文字})を打ちます。
    例:
    caseSensitive(?i)whOcAreSaBoutCases(?-i)nowicare

エスケープ文字

説明

特別な意味がある文字は普通な文字としたい時、その文字をエスケープするのが必要です。
例:. を打つ時、普通な(dot)じゃなくて、「\n以外、任意の文字」という意味が持っていますから、普通な(dot)としたい時、エスケープが必要です。

シンタックス:

  • \{特別な文字}
    例:\. ; \\ ; \+ ; \* ; \? など
  • 文のエスケープシーケンス:\Q{エスケープされたい文}\E
    例:\Q 3*4+5 = 6 \E → 量指定子に扱われなくて、普通の数学演算子に扱われます。
    上記のシンタックスを使うと、全て文字は文字通りに扱われます。

先読み・後読み言明 (LOOKAROUNDS)

定義

「先読み・後読み言明」は、アンカーと境界とそっくりですが、「先読み・後読み言明」はregexポインターの位置を一致する代わりに、文字を一致してみます。
例:
^文字は、文字に「ライン・文字列の開始」を一致してみる
(?<={パターン}){文字}は、{文字}に「パターン」を一致してみる

幅0のアサーション・Zero-Length Assertion

アンカーと境界と同じですが、「先読み・後読み言明」にはっきり見えます。

Zero-Length Assertion:

Regexは文字を合致してみるが、合致する場合は、合致した文字を集合しなくて、放置されます。
普通の処理は、Regexは文字を合致した後、regexのポインタは次の文字列の文字に進みますが、アンカーと「先読み・後読み言明」には、合致した後、ポインタが進みません。それで、次の処理はまだその文字を合致してみます。
例:
q(?=u)it ー quitを合致しない。
解説:
qの後ろはuということは正しいですから、後読みは成功でした。
ですが、regexのポインタが進まないので、次の処理はregexのiが文字列のuを合致してみます。合致しないので、regexが失敗です。quitを戻したい場合は、正しいパターンは q(?=u)uitです。

種類

基本的には、

  • 肯定先読みアサーション : 文字(?=パターン)
    文字の後ろ、パターンを合致してみる。合致する場合は正しい

  • 否定先読みアサーション : 文字(?!パターン)
    文字の後ろ、パターンを合致してみる。合致しない場合は正しい

  • 肯定後読みアサーション : (?<=パターン)文字
    文字の前、パターンを合致してみる。合致する場合は正しい

  • 否定後読みアサーション: (?<!パターン)文字
    文字の前、パターンを合致してみる。合致しない場合は正しい

下のテキストを検討してください:

123JPY #(1)
4567JPY #(2)
890IDR #(3)

\d{3}(?=JPY)で合致する場合は、(1)の123と (2)の567を戻します。 \d+(?!JPY)で合致する場合は、(1)の12、(2)の456、と (3)の890を戻します。

\d{3}(?=JPY)の説明:

  • 123JPY -> 123
    1. \d{3}123を合致します。
    2. 123の後ろにJPYがあるかどうか(?=JPY)で検証します。123の後ろにJPYが実際にあるので、検証は成功します。
    3. ですが、後読みは合致された文字列を集合しませんから、JPYが集合されません。
    4. それで、合致の結果は以前に集合した123です。
  • 4567JPY -> 567
    1. \d{3}456を合致します。
    2. 456の後ろにJPYがあるかどうか(?=JPY)で検証します。456の後ろにJPYがないので、検証は失敗します。それで、合致処理が失敗します。
    3. バックトラッキングで、\d{3}の合致を繰り返します。
      1. 今回、456を合致しなくて、567を合致します。
      2. 567の後ろにJPYがあるかどうか(?=JPY)で検証します。567の後ろにJPYがあるので、検証は成功します。
      3. ですが、後読みは合致された文字列を集合しませんから、JPYが集合されません。
    4. それで、合致の結果は以前に集合した567です。
      2番目の例に567を戻したくない場合は:
    5. \d{3}(?=JPY)の冒頭に ^を追加します。
    6. 文の中に検索する場合は、\d{3}(?=JPY)の前後に\bを追加します。

\d+(?!JPY)の説明:

  • 890IDR -> 890
    1. \d+890を合致します。
    2. 890の後ろにJPYがないかどうか(?!JPY)で検証します。890の後ろにJPYがないので、検証は成功します。
    3. ですが、後読みは合致された文字列を集合しませんから、JPYが集合されません。
    4. それで、合致の結果は以前に集合した890です。
  • 4567JPY -> 456
    1. \d+4567を合致します。
    2. 4567の後ろにJPYがないかどうか(?!JPY)で検証します。4567の後ろにJPYがあるので、検証は失敗します。それで、合致処理が失敗します。
    3. バックトラッキングで、\d+の合致を繰り返します。
      1. 今回、4567を合致しなくて、456を合致します。
      2. 456の後ろにJPYがあるかどうか(?!JPY)で検証します。456の後ろにJPYがないので、検証は成功します。
      3. ですが、後読みは合致された文字列を集合しませんから、7JPYが集合されません。
    4. それで、合致の結果は以前に集合した456です。
  • 123JPY -> 上と全く同じ解説で、12を戻します。

私の期待は\d+(?!JPY)で合致すると、4567JPY123JPYから何も戻したくないんですが、期待がミスしてしまいました。
このような挙動があるので、先読みを使う時、注意しなければなりません。

最後に

正規表現を勉強する前に、大きな一覧に多い項目の編集が必要の時に、パターンがわかるのに、正規表現がわからないので一個ずつ編集しなければなりません。それはすごく面倒でした。
正規表現の基本的な特性を生かせるようになったので、そのようなタスクは一つコマンドだけでやり遂げることができます。本当に便利です! さらに、正規表現でテキスト検証(正規表現を使わなければすごく難しくなります)を実装することができるようになりした。

12/1から25日間にわたって更新してきた Tech Kayac Advent Calendar 2017 もこれが最後の記事となります。お楽しみいただけましたでしょうか。 来年もまたよろしくお願いします! メリークリスマス!🎄

カヤックでは最新鋭の技術で開発したいエンジニアを募集しています!