個人開発のWebサービスをリリースして2ヶ月で見つかったバグを一挙紹介する

この記事は、[ひとり開発 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/solo-developer)の2日目の記事になります。
1日目の記事はhimataniさんの[ひとり開発でも諦めない、これからのプロダクトのつくり方](https://note.com/himatani/n/n94067db6283e)でした。

## はじめに

こんにちは、ぷらす ([@p1ass](https://twitter.com/p1ass))です。

皆さんは個人開発をする時にしっかりとテストやデバッグをしていると言い切れますか？

私は趣味レベルで開発しているときはスピード優先で**テストをないがしろ**にしていまうときがあります。しかし、**大抵バグを埋め込んでしまい、後々大変なことになってしまいます**。

この記事では、私が個人で開発している[Memoito（めもいと）](https://memoito.com)を開発・運用していく上で見つかったバグ・不具合を自戒を込めて一挙紹介していきます。

この記事を読んで、今一度、自分の開発しているサービスを見直すきっかけになれば幸いです。

## Memoitoとは

最初に、この記事で紹介していくバグが発見されたWebサービスである[Memoito](https://memoito.com)について先に紹介しておきます。

[Memoito](https://memoito.com)はTwitter連携を用いて、メモを**フォローしている人と紐付けて**保存することができるWebサービスです。

![紹介資料](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191796/2af95f58-a5bb-3779-f97e-9892ccf6b9c3.jpeg)

Twitterで気軽に連絡先を交換できるようになりましたが、徐々にフォローしている人が増えてきて、**「この人誰だっけ？」**となる経験はないでしょうか？

[Memoito](https://memoito.com)はフォローしている人と紐付けてメモを保存できるので、スマホの標準メモアプリでメモするより簡単にメモを取ることができます。

勉強会やミートアップ、カンファレンス等で会った人のことを、その時話した内容と一緒にメモを取れば、後から見直すことができてとても便利です。

公式Twitterもあります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Memoito（めもいと）はSNSの繋がりと紐付けてメモを保存できるWebサービスです！<br><br>フォローした理由をメモしておいたり、オフ会で会った時に話した内容をメモしたりできるので是非使ってみてください！<a href="https://t.co/r7rzQ9sXI4">https://t.co/r7rzQ9sXI4</a></p>&mdash; Memoito（めもいと）公式 (@MemoitoOfficial) <a href="https://twitter.com/MemoitoOfficial/status/1178652208457404426?ref_src=twsrc%5Etfw">September 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## アーキテクチャ

サービスのアーキテクチャが分かっていた方が記事を読みやすいと思うので、軽く説明します。詳しいことは、今度書く予定の記事を参照してもらうとして、こんな感じのアーキテクチャになっています。

**フロントエンド**

- Nuxt.js 
- Netlify

**バックエンド**

- Go

**インフラ**

- GKE
- CloudSQL

デプロイはCircleCIに寄せていて、Dockerイメージのpushや`kubectl apply`などを実行しています。また、GCPのインフラ構成定義にはTerraformを使っています。

個人ではオーバーエンジニアリングですが、頑張って今風なアーキテクチャにしています。

## 見つかったバグ

さて、ここからは見つかったバグを紹介していきます。

### リリース前の検証ユーザがDBに残っている

#### 問題
リリース前に無効なIDを持つTwitterのユーザ情報をインサートしてしまっていて、それがDBに残ったままでした。

```sql
mysql> select id from twitter_users where id = 0;
+----+
| id |
+----+
|  0 |
+----+
1 row in set (0.04 sec)
```

ここでの`id`はTwitter側の内部IDと同じ値なので、0になることはありません。
おそらく、ゼロ値の構造体をそのままインサートしてしまっていたが原因と考えられます。

#### 対応

直接本番DBのコンソールからrowを消しました。

ただ、この作業はさくっとSQLを1回発行するだけではダメでした。
SQLの制約がいくつか張られていたので、他のテーブルのrowを先に削除する必要がありました。

間違って他のユーザのデータを消すわけにはいかないので、入念に調査をして、冪等性があるクエリでrowを削除するようにしました。


### 無効な値を持つレコードがDBに格納されている

#### 問題

これも上と似ていますが、本来存在し得ない値が書き込まれていました。

たとえ話ですが、ある状態を表すカラム`status`は本来、[0, 2]の値を取るはずが、3が書き込まれている、といった状況でした。

これは、JSON APIでPOSTされた値のバリデーションが漏れていたのが原因でした。

#### 対応

全てのフィールドに対して、値が有効であるかを確認するバリデーション関数を実装し、テストもしっかりと書きました。

いくらクライアントサイドでバリデーションしているとはいえ、サーバ側でも実装しなきゃなという気持ちになりました(当たり前)。

### 1対1で対応しているはずのテーブルのレコード数が違う

#### 問題

[Memoito](https://memoito.com)は、将来的に他のSNSの対応を見据えているため、ユーザ情報は汎用的な`users`テーブルとTwitter固有の情報を格納している`twitter_users`テーブルに分けて保存しています。

現時点では、Twitterしか対応していないため、`users`テーブルと`twitter_users`テーブルのrowの数は一致するはずですが、なぜか一致していませんでした。

```sql
mysql> select count(id) from users;
+-----------+
| count(id) |
+-----------+
|       200 |
+-----------+
1 row in set (0.05 sec)

mysql> select count(id) from twitter_users;
+-----------+
| count(id) |
+-----------+
|       184 |
+-----------+
1 row in set (0.04 sec)
```

これは、トランザクションが正しく貼れていなかったことが原因で、片方が失敗した場合に不整合が生じていました。

#### 対応

まず、２つのテーブルに対するインサートを同じトランザクションで実行するようにアプリケーションを修正しました。

その後、おかしいrowを探し出し、SQLでパッチを当てていきました。

具体的には、

1. 不必要な`users`のrowを探す
2. その`id`が使われているメモ(`notes`テーブル)を探す
3. `notes`の`user_id`を正しいものに変更する
4. 参照が全てなくなった不必要な`users`のrowを削除

という手順で対応しました。

これが、一番修正が面倒くさいバグでした。
おかしなrowの洗い出しが大変でしたし、SQLの制約を考慮しつつパッチを当てなくてないけないのでなかなか骨の折れる作業でした😇


### Twitterのプロフィールが更新されない (未解決)

### 問題

[Memoito](https://memoito.com)ではTwitterのプロフィールをこちら側のDBに保存しています。
登録ユーザはログインするたびに新しいプロフィールに更新されるようになっています。

しかし、メモを取った相手は必ずしも登録ユーザではないため、プロフィールが更新されていませんでした。

結果として、プロフィールアイコンが404で表示されないという不具合がありました。

### 対応

実はこの不具合はまだ修正できていません。せっせと実装中です。


DBに保存されているユーザの全てのプロフィール情報をTwitter APIから取得して、プロフィール情報を更新する方法を考えています。

おそらく、一番オーソドックスな方法なのではないでしょうか。他に良い方法があれば教えて下さい。

## おわりに

ここまで、リリースしてから見つかったバグを紹介してきました。

特に整合性周りがきちんと実装していないと、不具合が生まれがちなので次からはしっかりと考えて実装しようと思いました。(対応大変なので、、、)

皆さんもこの記事を反面教師として、今一度自分のアプリケーションを見直してみましょう。何かヤバいものが見つかるかもしれません、

最後になりますが、よかったら[Memoito](https://memoito.com)使ってみてください！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Memoito（めもいと）はSNSの繋がりと紐付けてメモを保存できるWebサービスです！<br><br>フォローした理由をメモしておいたり、オフ会で会った時に話した内容をメモしたりできるので是非使ってみてください！<a href="https://t.co/r7rzQ9sXI4">https://t.co/r7rzQ9sXI4</a></p>&mdash; Memoito（めもいと）公式 (@MemoitoOfficial) <a href="https://twitter.com/MemoitoOfficial/status/1178652208457404426?ref_src=twsrc%5Etfw">September 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

明日は[ひとり開発 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/solo-developer)はbinnmtiさんの担当です。お楽しみに。