1. はじめに
サービスを開発していると、画面を実装することよりも、その後のことを考える時間のほうが長くなる場合があります。
最近、観光地コンテンツサービスのレビュー機能を開発した際に、まさにそのような経験をしました。
レビュー機能とは、ユーザーが自分で感想を作成し、写真を登録できる機能です。
機能自体は比較的単純に見えます。ユーザーがレビュー内容を入力して画像をアップロードすると、サーバーに保存し、保存されたデータを再び画面に表示すればよいのです。
最初は、レビューが正常に登録され、閲覧できることに集中していました。しかし、機能の実装を終えた後、運用環境を基準に改めて見直してみると、思った以上に多くの疑問が生じました。
-
ユーザーが想定外の文字列を入力したらどうなるのか。
-
HTMLタグやスクリプト形式の入力が保存されたらどうなるのか。
-
画像ではないファイルを画像のようにアップロードしたらどうなるのか。
-
レビューの編集過程で、ファイル情報と実際の保存データが互いに異なる状態になったらどうなるのか。
開発環境では問題がなさそうに見えた機能も、運用の観点から改めて見直すと、補完すべき点が少なくありませんでした。
今回の記事では、レビュー機能を改善する過程で適用した 入力値の検証、画像アップロードの検証、そしてデータ整合性の管理に関する経験を共有したいと思います。
2. 運用環境で発見した問題
レビュー機能は、ユーザーが直接コンテンツを生成する代表的な機能です。
管理者画面とは異なり、ユーザーがどのような値を入力するかを予測するのは難しく、開発者が意図していない方法で機能を使用する可能性もあります。
運用環境を基準に検討した結果、次のような問題を発見しました。
特にレビュー機能はユーザーが直接入力する領域であるため、通常の利用シナリオだけでなく、想定外の入力や例外的な状況まで併せて考慮する必要がありました。
|
区分 |
発見した問題 |
影響 |
|---|---|---|
|
テキスト入力 |
同じ意味の文字列が異なる形式で保存される可能性がある |
データ品質の低下 |
|
ユーザー入力 |
HTMLタグおよびスクリプト形式の入力が可能 |
セキュリティおよび運用上のリスク |
|
ファイルアップロード |
MIMEタイプだけで画像かどうかを判断 |
不正なファイルがアップロードされる可能性 |
|
画像編集 |
ファイル情報とデータ情報が一致しない可能性がある |
データ整合性の問題 |
3. テキスト入力値の検証を適用
最初に改善したのは、レビュー本文と作成者名に対する入力値の処理でした。
最初は、ユーザーが入力した文字列をそのまま保存していました。
しかし、同じ意味を持つ文字列でも、入力方法によって異なる形式で保存される可能性があることが目につきました。
代表的な例として、全角文字と半角文字はユーザーの目には似て見えますが、システムでは互いに異なるデータとして認識されます。
このようなデータが蓄積すると、検索やデータ管理の過程で予期しない問題が発生する可能性があります。
これを解決するため、保存前に文字列の正規化処理を追加しました。
String normalized = Normalizer.normalize(value, Normalizer.Form.NFKC).trim();
if (normalized.indexOf('<') >= 0 || normalized.indexOf('>') >= 0) {
throw new IllegalArgumentException("Review text cannot contain angle brackets.");
}
NFKC(Normalization Form Compatibility Composition)を適用して文字列を正規化し、<、>文字が含まれている場合は保存を制限するように修正しました。
当初は、わいせつな文言や不要なタグの入力を防ぐことが主な目的でした。
しかし、検討を進める中で、HTMLタグやスクリプト形式の入力が保存される可能性についても併せて考慮するようになりました。
現在のサービス構造ですぐに問題が発生しなくても、保存されたデータが別の画面や機能で利用される場合、XSS(Cross Site Scripting)のような問題が発生する可能性があります。
結果として、この検証は単なる特殊文字の制限ではなく、データ品質とサービスの安定性を併せて考慮する作業となりました。
4. フロントエンドとバックエンドによる二重検証構造
入力値の検証を適用するにあたり、もう一つ悩んだのは、どこで検証を行うかという点でした。
当初は、バックエンドだけで検証しても十分だと考えていました。
しかし、ユーザーは保存ボタンを押した後になって初めてエラーを確認できたため、ユーザー体験の面では改善の余地がありました。
そこで、フロントエンドでも同じ検証ルールを適用しました。
const hasUnsafeReviewText = (value: string) => {
const normalized = value.normalize('NFKC');
return normalized.includes('<') || normalized.includes('>');
};
フロントエンドではユーザーの入力時に即座に検証して迅速なフィードバックを提供し、バックエンドでは保存直前に同じルールで最終検証を行う構成にしました。
各領域の役割は次のとおりです。
|
区分 |
役割 |
|---|---|
|
フロントエンド |
入力時の即時検証およびユーザーフィードバックの提供 |
|
バックエンド |
保存直前の最終検証およびデータ保護 |
この構造を適用した後、ユーザー体験とデータの安定性をともに確保できました。
特に、フロントエンドの検証だけではAPIの直接呼び出しや迂回リクエストを防げないため、バックエンドの検証は必須であることを改めて確認できました。
5. MIMEタイプの検証から実際の画像検証へ
今回の作業で最も悩んだのは、画像アップロードの検証でした。
初期実装では、ファイルのMIMEタイプのみを確認していました。
たとえば、image/pngやimage/jpegのようなタイプで渡された場合に、画像ファイルだと判断する方式でした。
当時はそれで十分だと考えていました。
しかし、検討の過程で「画像として渡されたファイルは、本当に画像なのだろうか?」という疑問が生じました。
MIMEタイプはクライアントが渡す情報にすぎず、実際のファイル内容を保証するものではありません。
これを補うため、フロントエンドではファイルシグネチャ(Signature)を確認し、JPEG、PNG、GIF、BMP形式であるかを先に検証するよう修正しました。
そしてバックエンドでは、実際に画像をデコードできるかを確認する検証ロジックを追加しました。
private boolean isDecodableRasterImage(MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
BufferedImage image = ImageIO.read(inputStream);
return image != null
&& image.getWidth() > 0
&& image.getHeight() > 0;
} catch (IOException e) {
return false;
}
}
検証基準をMIMEタイプから実際の画像データへ拡張することで、画像ではないファイルがアップロードされる状況をより確実に防止できるようになりました。
6. 画像編集時のデータ整合性管理
個人的に最も印象に残った作業は、画像編集機能の改善でした。
ユーザーはレビューの編集時に、次のような操作を行うことができます。
-
既存の画像を維持
-
既存の画像を一部削除
-
新しい画像を追加
-
既存の画像をすべて削除
ユーザーから見ると単純な編集機能のように見えますが、実際には複数のデータが同時に変更されます。
実際のファイル、ファイル識別情報(clipId)、サムネイル情報、レビューのデータが常に同じ状態を維持していなければなりません。
もし一部の情報だけが変更されると、実際のファイルは削除されたのにレビューのデータが残っていたり、反対にデータは削除されたのにファイルが存在し続けたりする状況が発生する可能性があります。
これを防ぐため、維持するファイルと削除するファイルを明確に区別し、関連するデータが同時に変更されるようロジックを整理しました。
また、最後の画像が削除される場合には、サムネイル情報と関連する識別情報も併せて削除するよう改善しました。
この作業を進める中で、機能を実装することよりも、データ間の関係を一貫して維持することのほうが重要な場合があると実感しました。
7. 適用結果
今回の改善作業により、次のような効果を得ることができました。
|
改善項目 |
適用結果 |
|---|---|
|
文字列の正規化 |
データの一貫性向上 |
|
HTMLタグの遮断 |
XSS発生の可能性低減 |
|
画像検証の強化 |
不正なファイルのアップロード防止 |
|
フロントエンド・バックエンドによる二重検証 |
ユーザー体験および安定性の向上 |
|
データ整合性の管理 |
ファイル情報の不一致の低減 |
個々の作業だけを見ると、小さな修正のように見えるかもしれません。
しかし、実際の運用環境では、このような小さな検証ロジック一つがサービス品質に大きな影響を与えることを実感しました。
8. まとめ
今回の作業を進める中で、機能の実装と運用の安定性は異なる観点であることを改めて感じました。
画面が正常に動作するだけでは、サービスが完成したとは言い難いものです。
ユーザーが予期しない入力を行った場合でもシステムが安定して動作し、複数のデータが常に一貫した状態を維持できなければなりません。
特に入力値の検証とデータ整合性の管理は、ユーザーが直接実感しにくい領域です。
しかし、このような目に見えない部分こそが、最終的にサービス品質を決定するのだと思います。
今回の経験を通じて、機能の実装だけでなく、データがどのように保存・管理されるのか、運用中にどのような問題が発生する可能性があるのかまで併せて考える習慣の重要性を、改めて実感することができました。
今後も機能の実装だけにとどまらず、運用環境まで考慮した開発を継続的に実践していきたいと思います。
zero