この記事は Link-U Technologies Advent Calendar 2025の22日目です
iOS からバックエンドに転向して 3 か月。
この記事はiOS からサーバー側に移った / これから移りたい人向けに、「どこでつまずきやすいか」と「どう考えると楽になるか」をまとめました。
はじめに
LUTサーバー班の野田佑樹です。
私は2020年から新卒で働き始め、ついこの間までiOSエンジニアをしていました。
2025年9月からLUTにジョインしましたが、今までのiOSエンジニアではなく、バックエンドとしてジョインしました。
元々、前職ではiOSエンジニアの肩書きではあるものの、バックエンドとiOS関係なくどちらも開発をしていました。
しかしバックエンドはシステムの都合上そこまで込み入った開発は無く、簡単なAPIの修正程度でした。
そのため実質LUTからバックエンドエンジニアをやっている状態です。
そんな自分がiOSエンジニアからバックエンドエンジニアになって、感じたことをざっくばらんにお話ししようと思います。
1. iOS 出身ほどやりがちな罠
サーバーを書き始めて最初にぶつかったのは、仕様の理解度がそのままコードの質に出るという点でした。
iOS では UI に近い層ほどビジネスロジックを薄くしがちで、
見えている範囲だけで物事を考えてしまいがちです。
その癖を持ったまま API や DB を設計すると、
裏側で必要だった状態を抜かしてしまいます。
極端ですが2つの例を紹介します
フロント脳でDB設計をすると
自分が一番「フロント脳だな……」と感じたのは、作品につけるタグの設計でした。
あるサービスで作品にタグをつける仕様があり、「タグは最大3件まで」とだけ要件に書かれていました。
そこで最初に頭に浮かんだのは、tag_1_id, tag_2_id, tag_3_id のようなカラムを作品テーブルに生やす設計です。
UIだけを見ていると、「最大3つまで並べて表示できればいいよね」と考えてしまいます。
テーブル定義だけを見ても、一見シンプルで分かりやすそうに見えます。
しかし、バックエンドとして腰を据えて考え始めると、すぐに違和感が出てきました。
- タグの上限が3件から5件に変わったらどうするのか
- 将来、タグに並び順や重みづけ(おすすめ度など)を持たせたくなったらどうするのか
- 別の種類のコンテンツ(作品以外)にもタグを付けたくなったら、また同じようなカラムを生やすのか
このあたりを考えれば考えるほど、「これは中間テーブルで表現したほうが良い関係では?」 という当たり前の結論に戻ってきます。
重い腰を上げて、結局は作品とタグの間に中間テーブルを作る形に落ち着きました。
振り返ってみると、最初に tag_1_id 〜 tag_3_id を思いついたのは、「UI上の制約(最大3つ)」をそのままDB設計に持ち込んでしまっていたからだと思います。
フロントの感覚だと、「画面に3つしか出さないから、データも3つでいい」という発想になりがちです。
一方で、サーバー側では「ビジネス的にどんな関係性を持つデータなのか」「将来どう拡張されうるのか」を先に考え、その上でUIには3つだけ出す、という順番で考えないといけません。
実はフロントをやっていた頃から、「仕様上の上限をコードに直書きする」「UIの都合で設計を決める」といった罠は身近にありました。
ただ、そのときはローカルな画面の中だけで完結することが多く、DBや他サービスとの関係まで含めて破綻することを、身をもって味わう機会が少なかったのだと思います。
フロントから見えない状態
もう一つ印象的だったのが、作品の審査状態にまつわる話です。
あるサービスでは、運営側が作品を事前にチェックするフローがありました。
しかし、作家側のWebフロントからは「審査中」や「差し戻し中」といったステータスは一切見せない、という仕様になっていました。
フロントだけを見ていると、作家が見るのは「公開されているかどうか」くらいです。
そのため、最初にDB設計を考えたとき、審査状態そのものを表すカラムをすっかり抜かしてしまっていました。
「ユーザーから見えない=アプリとして扱わなくていい」と、無意識に判断してしまっていたのだと思います。
ところが、のちほど運営側の管理画面の実装に入ったときに、その抜けが一気に表面化しました。
運営からすると、
- いまどの作品が審査待ちなのか
- 誰がいつ申請したのか
- 差し戻した理由は何なのか
といった情報が、日々の業務の中で必須になります。
ここで初めて、「あれ、この状態を持つカラムやテーブルを、DB側にちゃんと用意していないと管理画面が作れない」という当たり前の事実に気がつきました。
これはもう、「今までフロントだったから、運営側の視点(業務フロー)を想像しきれていなかった」という話だと思います。
実装の考え方自体はフロントもサーバーも大きくは変わらないはずで、もしフロント時代に「運営側もこの画面を使う」「審査フローを可視化したい」という要件をちゃんと聞けていれば、同じように状態を設計していたはずです。
違うのは、「誰のための状態なのか」をどこまで意識して設計テーブルに落とし込むかという点でした。
フロントにいると、「画面に出る情報」がどうしても主役になります。
一方でサーバーにいると、「画面には出ないけれど、業務や後続処理のために絶対に必要な状態」のほうを、先に考えないといけません。
バックエンドに来て、UIからは見えない“関係性そのもの”をどう表現するかを考える場面が一気に増えたことで、「あ、これはフロントの感覚のままだと危ないな」と強く自覚するようになりました。
この経験を通じて、「フロントに見えない=重要ではない」ではなく、「フロントに見えない情報ほど、むしろ業務ロジックの中では重要なことが多い」という感覚が少しずつ身についてきたと感じています。
2. cron とバッチ処理の「見えなさ」に振り回される
最初に cron と本格的に向き合わされたのは、既存の処理を改修していたときでした。
仕様変更に合わせて処理を直そうと、いつも通りソースコードから流れを追っていきます。
ところが、ある地点までは順調に追えるのに、その先でロジックがぷつっと途切れてしまいました。
「ここから先でデータが更新されていてほしいのに、コード上にそれらしい処理が見当たらない」。
ログを見ても決定的な手がかりがなく、だんだんと 「そもそもこの処理、本当に動いているのか?」 という気持ちになってきます。
このとき、自分の頭の中には cron という選択肢がまったく入っていませんでした。
cron 自体を知らなかったわけではないのですが、「今追っているこの処理が cron で動いているかもしれない」という発想が、完全に抜け落ちていたのです。
行き詰まってチームの方に聞いてみると、「それ、サーバーの cron から叩いてるよ」と一言で教えてもらえました。
実際にサーバーの設定を覗いてみると、その処理らしきジョブが登録されていました。
さらに辿っていくと、cron は単にリポジトリ内のスクリプトを呼び出しているだけで、そのスクリプトの中に、例のテーブルへ INSERT している箇所が見つかります。
最後は、そのテーブル名を手がかりにログや他の処理と突き合わせながら、「ああ、ここでデータが増えていたのか」とようやく全体像がつながりました。
この一件で強く感じたのは、フロント側では「全部コード上で追える」ことに慣れすぎていたということです。
UI のボタンを起点に画面→コード→API と一本道で辿れる世界から来ると、「時間をきっかけに、コードの外側(cron)から処理が始まる」ことに、頭がなかなか切り替わりません。
その後の改修では、まず「このデータは誰が、いつ、どこから書き換えているのか?」を整理するようにしました。
ユーザー操作や管理画面だけでなく、「もしかしてこれは時間起動のバッチが触っているのでは?」 という発想を最初から候補に入れるようになったのは、この出来事がきっかけです。
一方で、本音を言うと いまでもできるなら cron にあまり仕事をさせたくない と感じています。
時間起動の処理が増えるほど、「どこで何が動いているのか」「どのタイミングで実行されるのか」が見えにくくなり、調査や改修のコストが一気に上がるからです。
そのため、今の案件ではなるべく、状態変化を契機に処理をさせるように仕様を持っていくようにしています。
3. 処理時間の概念がまったく違う
処理時間について一番考えさせられたのは、画像ファイルから EPUB を生成するバッチを検討していたときでした。
作品が投稿されたら、その画像群を元に EPUB ファイルを作る、というイメージです。
このときまず悩んだのは、「いつまでに作れれば良いファイルなのか」 という締め切りの設定でした。
作品投稿のタイミングで同期的に EPUB を作ってしまうこともできますが、
ー そもそも EPUB は即時に必要なものではない
- 仮に1日遅れても、致命的な問題にはならない
- フロント脳としては「重そうな処理は極力非同期にしたい」
という事情があり、最終的に cron で定期的に作る という方針を取りました。
そうすると今度は、「1分ごとに少しずつ作るか」「もっと間隔を空けて一気に作るか」 という別の悩みが出てきます。
本来であれば、「1分あたり何作品くらい投稿されるのか」「1作品あたりの平均処理時間はどれくらいか」といった前提から逆算して、バッチの間隔や1回あたりの処理件数を決めるべきです。
しかし、当時は詳しい要件を決めきれておらず、とりあえず 1 分ごとに実行して、遅くなってきたらあとで間隔を伸ばせばいいだろう という雑な決め方をしてしまいました。
結果としてこの処理は、そもそも要件外のものだと後から分かり、最後まで作り切ることはありませんでしたが、「締め切り」と「処理能力」のバランスをちゃんと設計する必要性だけは強く印象に残りました。
リトライの設計についても、このバッチではシンプルに、
- 処理が成功しない限り、状態を「成功」にしない
- 次回以降のスクリプト実行時に、まだ成功していない作品を拾って再チャレンジする
という方針を取りました。
バッチ内部で細かくリトライ回数や間隔を制御するのではなく、「失敗した作品は次の実行サイクルでまとめて拾い直す」イメージです。
フロントにいた頃は、「ユーザーにもう一度ボタンを押してもらう」「画面をリロードしてもらう」といった形で、リトライの責任をある程度ユーザー側に預けることができました。
一方でサーバーに来ると、時間で動く処理のリトライ戦略そのものを、自分で設計しておかないといけない という感覚が強くなりました。
フロント側にいると、「処理が遅い」ときの影響範囲は、基本的にその画面を開いているユーザー本人に閉じています。
スピナーを出したり、進捗バーを出したり、「時間がかかっています」とメッセージを出したり、最悪は「画面を閉じてやり直してください」で逃げることもできます。
ユーザー体験としてはもちろん良くないのですが、被害の範囲は比較的分かりやすく、「目の前の1ユーザーの UX が悪化する」という話で済みます。
サーバー側の「遅さ」は、ここがまったく違います。
例えば、1分ごとに起動するバッチが、何らかの理由で毎回5分かかるようになってしまったとします。
そうすると、
- バッチ同士がどんどん重なり合って、サーバーリソースを食いつぶしていく
- その影響で、関係ない API のレスポンスまで遅くなる
- 遅くなった結果、今度はフロント側からのリトライが増えて、さらにサーバーが苦しくなる
といった形で、「遅さ」のしわ寄せが別のジョブや別のユーザーに波及していきます。
フロントでは「この画面だけ重い」という状態で済んでいたものが、サーバーでは「この処理が重いせいで、関係ない画面まで重くなる」という形で現れやすい。
遅い処理を1つ許容すると、その負債を「後続のバッチ」や「他のユーザー全員」に静かに押し付けてしまう、というイメージに近いと感じています。
この違いを意識するようになってからは、「1分ごとに動くなら 1分以内に必ず終わるように設計する」「終わらない場合はどこかで締め切りを切る(タイムアウトする)」という発想を、意識的に持つようになりました。
フロントのときは「ユーザーに少し待ってもらえばいいか」で済ませていた部分が、サーバーでは「システム全体の健康状態を守るために、どこで線を引くか」という設計そのものになってくるのだと、少しずつ実感しているところです。
逆に言うと、ここまで考えられていれば初期の事故はだいぶ防げるのですが、本当に怖いのは「サービスが続いてから」です。
数年たったあと、誰もちゃんと覚えていない cron バッチが静かに失敗し続けていて、ある日ふと「そういえばこの集計、ずっと更新されていなくない?」と気づく──みたいな未来が普通にありそうで怖いなと感じています。
だからこそ、設計段階で「いつ・何を・どれくらい処理するか」を決めるだけでなく、「失敗したらどう検知するか」「誰が気づけるようにしておくか」まで含めて考える必要があるのだろうな、と今は思っています。
まとめ:いまは“転向初期の違和感”の真っ只中
まだバックエンドをやり始めて 3 か月。
慣れてきたとはいえ、まだまだ体感ベースで学んでいる段階です。
ハードウェアからソフトウェアまで幅広い知識が必要で大変ではありますが、そこが少しずつ“視点の広がり”に変わっていく気がしています
最初の数ヶ月は戸惑って当然なので、「分からないまま進めない」だけ気をつければOKだと思っています。
最後まで読んでいただきありがとうございました。