レガシーシステムの保守から得られた知見

# 前置き

私はIT業界に入ってから約8年になります。
新規開発案件に入ることもありましたが、保守や保守兼追加改修のプロジェクトに入ることが多く、7～8割は保守兼追加改修の担当をしてきました。

新規開発案件で今流行りの技術スタックで開発できる環境のほうが楽しいと思っていましたが、レガシーなシステムと向き合うことで得られたことも大きかったので、その経験を共有しようと思います。

# レガシーシステムの保守・改修で得られたもの

## 生のアンチパターンに触れることができた

新しい言語やフレームワークでの実装を学習する際、なにかの教材を参考に学習していくことが多いと思いますが、そのような教材の場合きれいな実装であったり、お手本のような実装がされていることが多いと思います。

レガシー・・・それもバグが多く含まれるようなシステムの場合、アンチパターンが多く含まれています。そのようなアンチパターンに多く触れることで、「このような実装をするととんでもないことになる」というような実装の仕方を学ぶことができました。

これは教材であったり、まともな実装をしているシステムに携わるだけでは経験できなかった貴重な体験であると言えると思います。

## メモリや負荷が掛かる箇所を意識した実装ができるようになった

過去に携わったプロジェクトのとある一覧画面の表示の機能で、

- DBから取ってきた値をPHPのメモリに保持して、権限がなくて見えないものは捨ててフロントに返却
- DBから値を取ってくるときに必要な項目だけでなく`SELECT *`のような状態(※1)で取得
- ページングはフロントの処理で行っており、100万件対象となるデータがあれば100万件フロントに返す

というような実装をしているものがありました。

<details>
<summary>※1: 実際にはmongoDBを使っていたので...</summary>

[$project](https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/)がRDBの`SELECT col1, col2, ...`の部分に該当するので、`$project`で必要最小限の取得にする必要がありました。
mongoDBはJSONのような形でデータを保持し、1レコード中に配列を持つこともでき、
各レコードに配列長が1,000のカラムを持っていた場合、1000レコード抽出すると1000 × (1000 + 他カラム) 分だけのデータがDBから抽出され、PHPのメモリに保持するような事になっていました。
(mongoDBではレコード≒ドキュメント、カラム≒フィールドと呼びますがわかりやすいようにRDB的な言い方にしています。)

更にJSONの配列長が100ある中でも実際に必要なのはその配列の中の1つだけで凄く無駄なデータをメモリに抱えながら処理していました。

<details>
<summary>例えばこんな感じのデータで...</summary>

例としてフォルダにファイルを入れて管理する(GoogleDrive)のようなシステムで、
1つのフォルダにいくつかの動画ファイルを入れ、更にMediaInfoでメディア情報をDBに登録しているようなシステムのデータが以下のようなデータだったとします。

下記の例ではフォルダの中に3つのファイルを入れていますが、フォルダの中に100件だったとすると・・・
更にこれは1レコードあたりのデータなので、これが1,000件、10,000件・・・
しかもフロントでページングさせようとしているとすると全部取ってきている・・・

と、とんでもない量のデータを取得していることになりますね(苦笑)

<details>
<summary>データ例(長いので更に折りたたみ...)</summary>

```json
{
  "Name": "test",
  "DateTime": "2022-01-23 12:34:56",
  "User": "test taro",
  "Files": [
    {
      "FileName": "001.mp4",
      "MediaInfo": {
        "creatingLibrary": {
          "name": "MediaInfoLib",
          "version": "22.09",
          "url": "https://mediaarea.net/MediaInfo"
        },
        "media": {
          "@ref": "C:\\Users\\xxxx\\Videos\\001.mp4",
          "track": [
            {
              "@type": "General",
              "VideoCount": "1",
              "AudioCount": "1",
              "FileExtension": "mp4",
              "Format": "MPEG-4",
              "Format_Profile": "Base Media / Version 2",
              "CodecID": "mp42",
              "CodecID_Compatible": "isom/mp42",
              "FileSize": "216536424",
              "Duration": "1200.365",
              "OverallBitRate": "1443137",
              "FrameRate": "60.000",
              "FrameCount": "71989",
              "StreamSize": "1053199",
              "HeaderSize": "24",
              "DataSize": "215483241",
              "FooterSize": "1053159",
              "IsStreamable": "No",
              "Recorded_Date": "2022",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "File_Created_Date": "UTC 2022-10-03 14:22:15.140",
              "File_Created_Date_Local": "2022-10-03 23:22:15.140",
              "File_Modified_Date": "UTC 2022-10-03 14:22:18.686",
              "File_Modified_Date_Local": "2022-10-03 23:22:18.686"
            },
            {
              "@type": "Video",
              "StreamOrder": "0",
              "ID": "1",
              "Format": "AVC",
              "Format_Profile": "High",
              "Format_Level": "4.2",
              "Format_Settings_CABAC": "Yes",
              "Format_Settings_RefFrames": "3",
              "Format_Settings_GOP": "M=1, N=30",
              "CodecID": "avc1",
              "Duration": "1200.365",
              "Source_Duration": "1200.357",
              "BitRate": "1241547",
              "Width": "1920",
              "Height": "1080",
              "Stored_Height": "1088",
              "Sampled_Width": "1920",
              "Sampled_Height": "1080",
              "PixelAspectRatio": "1.000",
              "DisplayAspectRatio": "1.778",
              "Rotation": "0.000",
              "FrameRate_Mode": "VFR",
              "FrameRate": "60.000",
              "FrameRate_Minimum": "1.737",
              "FrameRate_Maximum": "133.929",
              "FrameCount": "71989",
              "Standard": "PAL",
              "ColorSpace": "YUV",
              "ChromaSubsampling": "4:2:0",
              "BitDepth": "8",
              "ScanType": "Progressive",
              "StreamSize": "186203579",
              "Source_StreamSize": "186203579",
              "Title": "VideoHandle",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "colour_description_present": "Yes",
              "colour_description_present_Source": "Stream",
              "colour_range": "Limited",
              "colour_range_Source": "Stream",
              "colour_primaries": "BT.601 NTSC",
              "colour_primaries_Source": "Stream",
              "transfer_characteristics": "BT.601",
              "transfer_characteristics_Source": "Stream",
              "matrix_coefficients": "BT.601",
              "matrix_coefficients_Source": "Stream",
              "extra": {
                "mdhd_Duration": "1200365",
                "CodecConfigurationBox": "avcC"
              }
            },
            {
              "@type": "Audio",
              "StreamOrder": "1",
              "ID": "2",
              "Format": "AAC",
              "Format_AdditionalFeatures": "LC",
              "CodecID": "mp4a-40-2",
              "Duration": "1200.345",
              "Source_Duration": "1200.346",
              "BitRate_Mode": "CBR",
              "BitRate": "192000",
              "BitRate_Nominal": "96000",
              "Channels": "2",
              "ChannelPositions": "Front: L R",
              "ChannelLayout": "L R",
              "SamplesPerFrame": "1024",
              "SamplingRate": "48000",
              "SamplingCount": "57616560",
              "FrameRate": "46.875",
              "FrameCount": "56171",
              "Source_FrameCount": "56171",
              "Compression_Mode": "Lossy",
              "StreamSize": "29279646",
              "StreamSize_Proportion": "0.13522",
              "Source_StreamSize": "29279646",
              "Source_StreamSize_Proportion": "0.13522",
              "Title": "SoundHandle / System sounds",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "extra": {
                "mdhd_Duration": "1200345"
              }
            }
          ]
        }
      }
    },
    {
      "FileName": "002.mp4",
      "MediaInfo": {
        "creatingLibrary": {
          "name": "MediaInfoLib",
          "version": "22.09",
          "url": "https://mediaarea.net/MediaInfo"
        },
        "media": {
          "@ref": "C:\\Users\\xxxx\\Videos\\002.mp4",
          "track": [
            {
              "@type": "General",
              "VideoCount": "1",
              "AudioCount": "1",
              "FileExtension": "mp4",
              "Format": "MPEG-4",
              "Format_Profile": "Base Media / Version 2",
              "CodecID": "mp42",
              "CodecID_Compatible": "isom/mp42",
              "FileSize": "216536424",
              "Duration": "1200.365",
              "OverallBitRate": "1443137",
              "FrameRate": "60.000",
              "FrameCount": "71989",
              "StreamSize": "1053199",
              "HeaderSize": "24",
              "DataSize": "215483241",
              "FooterSize": "1053159",
              "IsStreamable": "No",
              "Recorded_Date": "2022",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "File_Created_Date": "UTC 2022-10-03 14:22:15.140",
              "File_Created_Date_Local": "2022-10-03 23:22:15.140",
              "File_Modified_Date": "UTC 2022-10-03 14:22:18.686",
              "File_Modified_Date_Local": "2022-10-03 23:22:18.686"
            },
            {
              "@type": "Video",
              "StreamOrder": "0",
              "ID": "1",
              "Format": "AVC",
              "Format_Profile": "High",
              "Format_Level": "4.2",
              "Format_Settings_CABAC": "Yes",
              "Format_Settings_RefFrames": "3",
              "Format_Settings_GOP": "M=1, N=30",
              "CodecID": "avc1",
              "Duration": "1200.365",
              "Source_Duration": "1200.357",
              "BitRate": "1241547",
              "Width": "1920",
              "Height": "1080",
              "Stored_Height": "1088",
              "Sampled_Width": "1920",
              "Sampled_Height": "1080",
              "PixelAspectRatio": "1.000",
              "DisplayAspectRatio": "1.778",
              "Rotation": "0.000",
              "FrameRate_Mode": "VFR",
              "FrameRate": "60.000",
              "FrameRate_Minimum": "1.737",
              "FrameRate_Maximum": "133.929",
              "FrameCount": "71989",
              "Standard": "PAL",
              "ColorSpace": "YUV",
              "ChromaSubsampling": "4:2:0",
              "BitDepth": "8",
              "ScanType": "Progressive",
              "StreamSize": "186203579",
              "Source_StreamSize": "186203579",
              "Title": "VideoHandle",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "colour_description_present": "Yes",
              "colour_description_present_Source": "Stream",
              "colour_range": "Limited",
              "colour_range_Source": "Stream",
              "colour_primaries": "BT.601 NTSC",
              "colour_primaries_Source": "Stream",
              "transfer_characteristics": "BT.601",
              "transfer_characteristics_Source": "Stream",
              "matrix_coefficients": "BT.601",
              "matrix_coefficients_Source": "Stream",
              "extra": {
                "mdhd_Duration": "1200365",
                "CodecConfigurationBox": "avcC"
              }
            },
            {
              "@type": "Audio",
              "StreamOrder": "1",
              "ID": "2",
              "Format": "AAC",
              "Format_AdditionalFeatures": "LC",
              "CodecID": "mp4a-40-2",
              "Duration": "1200.345",
              "Source_Duration": "1200.346",
              "BitRate_Mode": "CBR",
              "BitRate": "192000",
              "BitRate_Nominal": "96000",
              "Channels": "2",
              "ChannelPositions": "Front: L R",
              "ChannelLayout": "L R",
              "SamplesPerFrame": "1024",
              "SamplingRate": "48000",
              "SamplingCount": "57616560",
              "FrameRate": "46.875",
              "FrameCount": "56171",
              "Source_FrameCount": "56171",
              "Compression_Mode": "Lossy",
              "StreamSize": "29279646",
              "StreamSize_Proportion": "0.13522",
              "Source_StreamSize": "29279646",
              "Source_StreamSize_Proportion": "0.13522",
              "Title": "SoundHandle / System sounds",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "extra": {
                "mdhd_Duration": "1200345"
              }
            }
          ]
        }
      }
    },
    {
      "FileName": "003.mp4",
      "MediaInfo": {
        "creatingLibrary": {
          "name": "MediaInfoLib",
          "version": "22.09",
          "url": "https://mediaarea.net/MediaInfo"
        },
        "media": {
          "@ref": "C:\\Users\\xxxx\\Videos\\003.mp4",
          "track": [
            {
              "@type": "General",
              "VideoCount": "1",
              "AudioCount": "1",
              "FileExtension": "mp4",
              "Format": "MPEG-4",
              "Format_Profile": "Base Media / Version 2",
              "CodecID": "mp42",
              "CodecID_Compatible": "isom/mp42",
              "FileSize": "216536424",
              "Duration": "1200.365",
              "OverallBitRate": "1443137",
              "FrameRate": "60.000",
              "FrameCount": "71989",
              "StreamSize": "1053199",
              "HeaderSize": "24",
              "DataSize": "215483241",
              "FooterSize": "1053159",
              "IsStreamable": "No",
              "Recorded_Date": "2022",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "File_Created_Date": "UTC 2022-10-03 14:22:15.140",
              "File_Created_Date_Local": "2022-10-03 23:22:15.140",
              "File_Modified_Date": "UTC 2022-10-03 14:22:18.686",
              "File_Modified_Date_Local": "2022-10-03 23:22:18.686"
            },
            {
              "@type": "Video",
              "StreamOrder": "0",
              "ID": "1",
              "Format": "AVC",
              "Format_Profile": "High",
              "Format_Level": "4.2",
              "Format_Settings_CABAC": "Yes",
              "Format_Settings_RefFrames": "3",
              "Format_Settings_GOP": "M=1, N=30",
              "CodecID": "avc1",
              "Duration": "1200.365",
              "Source_Duration": "1200.357",
              "BitRate": "1241547",
              "Width": "1920",
              "Height": "1080",
              "Stored_Height": "1088",
              "Sampled_Width": "1920",
              "Sampled_Height": "1080",
              "PixelAspectRatio": "1.000",
              "DisplayAspectRatio": "1.778",
              "Rotation": "0.000",
              "FrameRate_Mode": "VFR",
              "FrameRate": "60.000",
              "FrameRate_Minimum": "1.737",
              "FrameRate_Maximum": "133.929",
              "FrameCount": "71989",
              "Standard": "PAL",
              "ColorSpace": "YUV",
              "ChromaSubsampling": "4:2:0",
              "BitDepth": "8",
              "ScanType": "Progressive",
              "StreamSize": "186203579",
              "Source_StreamSize": "186203579",
              "Title": "VideoHandle",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "colour_description_present": "Yes",
              "colour_description_present_Source": "Stream",
              "colour_range": "Limited",
              "colour_range_Source": "Stream",
              "colour_primaries": "BT.601 NTSC",
              "colour_primaries_Source": "Stream",
              "transfer_characteristics": "BT.601",
              "transfer_characteristics_Source": "Stream",
              "matrix_coefficients": "BT.601",
              "matrix_coefficients_Source": "Stream",
              "extra": {
                "mdhd_Duration": "1200365",
                "CodecConfigurationBox": "avcC"
              }
            },
            {
              "@type": "Audio",
              "StreamOrder": "1",
              "ID": "2",
              "Format": "AAC",
              "Format_AdditionalFeatures": "LC",
              "CodecID": "mp4a-40-2",
              "Duration": "1200.345",
              "Source_Duration": "1200.346",
              "BitRate_Mode": "CBR",
              "BitRate": "192000",
              "BitRate_Nominal": "96000",
              "Channels": "2",
              "ChannelPositions": "Front: L R",
              "ChannelLayout": "L R",
              "SamplesPerFrame": "1024",
              "SamplingRate": "48000",
              "SamplingCount": "57616560",
              "FrameRate": "46.875",
              "FrameCount": "56171",
              "Source_FrameCount": "56171",
              "Compression_Mode": "Lossy",
              "StreamSize": "29279646",
              "StreamSize_Proportion": "0.13522",
              "Source_StreamSize": "29279646",
              "Source_StreamSize_Proportion": "0.13522",
              "Title": "SoundHandle / System sounds",
              "Encoded_Date": "UTC 2022-10-03 14:22:18",
              "Tagged_Date": "UTC 2022-10-03 14:22:18",
              "extra": {
                "mdhd_Duration": "1200345"
              }
            }
          ]
        }
      }
    }
  ]
}
```

---

</details>

</details>

</details>

数十件、数百件程度なら動きます。。。ﾊｲ

しかし、データ量が一定量を超えるとメモリ使用量オーバーやタイムアウトでシステムとしてまともに使えないような状態となっていました。

すぐには対応できそうに無かったそのシステムのPHPの`memory_limit`の設定値が`3G`という普通ではありえないような設定となっていたり、そのためにサーバ側のメモリ容量を上げたりという残念な対応をしていました。
(そのシステムの派生プロジェクトではそれ以上の数値となっているものもありました。)

また、メモリ制限オーバーのエラーが出ずとも動作的にすごく重たいのでお客さんにとっては相当なストレスだったと思います。

その後、(改修にかける時間的にも改修をし始められる時間を作るのにも)大分時間はかかりましたが以下のような改修を行い、まともに動くような実装にすることができました。

- DBから権限のあるものだけ抽出
- 一覧の表示に必要な項目だけを抽出
- サーバ側の処理でページングしてフロントに返す

改修後には`memory_limit`の値は一旦デフォルトの`128M`に戻した状態で、データ量を遥かに上回る数百万件のデータの表示であっても問題なく動くようにすることができました。
(クエリ化したことでDBサーバに負荷が掛かるようになっているので、まだ記事投稿時点では達成できていませんが、負荷の状況次第ではDBのレプリケーションやシャーディングで対応を行っていこうと考えています。)

実際にこういったメモリオーバーや効率の悪い実装に出会ったからこそ、気を付けながら実装ができるようになったと思います。


## 技術的負債という課題について考えるようになった

今日ではよく耳にする「[技術的負債](https://ja.wikipedia.org/wiki/%E6%8A%80%E8%A1%93%E7%9A%84%E8%B2%A0%E5%82%B5)」という言葉はレガシーなシステムに携わらなければ出会わなかった言葉かもしれません。

- 目先の売上のために設計や製造で妥協してしまい、その時に対応しておけば少ない工数で対応できたはずのものが遥かに多くの工数を掛けないと対応できない状態となってしまって、むしろ長い目で見ると赤字になってしまっていた
- 新しい機能を入れたくても元の実装の甘さが邪魔をして無駄な工数が発生してしまう
- そのような状態が放置されて何をするにも雪だるま式に工数がかかる状態となってしまう


このような課題に関しては [エンジニアリング組織論への招待](https://gihyo.jp/book/2018/978-4-7741-9605-3) や著者の[広木 大地](https://qiita.com/hirokidaichi) さんはQiita上にも投稿されていますので、とても参考になります。
![TH320_9784774196053.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127686/ea0e3a9e-a988-72ae-5d8f-8f0570841b93.jpeg)

今の時代のシステムは「作って売ったら終わり」というモデルは少なく、作って売って「保守しながら要望を受けて改善を繰り返す」というようなモデルがほとんどだと思います。

このため、短期的な売上でなく長期的な(保守や改修に掛かる工数も意識した)売上を意識しながら作ることができるエンジニアになれたことはとても大きな経験です。

(理想を言えばほぼ保守をせずとも安定稼働して保守費用をもらっている状態で、要望があればササッと作れるシステムが理想ですね・・・)


# とはいえど不満も多々…

## 放置されすぎ問題

直近携わっているパッケージソフトのプロジェクトはgitやsvnの履歴を見るにどうやら2012年にはある程度動くものが出来上がっていたようです。

そしてそれを「標準版」としてフォーク元として各お客さん向けに要望対応を行いながら導入、もしくは導入後に使いにくい部分があれば改修依頼という形で案件化して利益を得る仕組みとなっています。

このため、標準版で潰しきれていないバグやソースが汚いところは全て各お客さん向けに引き継がれている状態となっていて、お客さんが増えれば増えるほど保守費用がかさむ状態となっていました。

放置することなく標準版でバグ潰しがされた状態であれば...きれいな状態であれば...と何度思ったことか・・・

## 保守ってあんまり評価されない…

なぜ技術的負債は放置されてしまうのか？

自分なりに行き着いた答えとして **保守や既存顧客向けの改修が評価されにくい** というところにあると思います。

実際に今所属している組織ではすごく頑張ってパフォーマンス改善をしても評価されない、保守や性能改善、技術的負債の返済は軽視されがちで、新規開発の案件や新規の顧客向けの案件のほうが評価されやすいと感じています。

そう感じる人が多くなると既存システムの保守、レガシーなシステムに携わる人が少なくなってしまい、放置されてしまう状況となってしまい、さらなる負債になっているのでは無いかと考えます。

また、新規開発・新規顧客向け案件の方が上司や周りからの協力を得やすいとも感じています。
面倒なのはわかりますが、今放置して将来的に困ったり赤字の原因になるのであれば今すぐにでも対応をしたほうがいいのでは・・・？と思うのが正直なところではあります。
(直視したくない問題だとは思いますが、中には相談して機嫌が悪くなる人も居るのも事実です。)

そういった組織の仕組み自体に問題があり、嫌になった人が退職して人員が流出し新しい人員を確保したとしても教育のために時間もお金もかかり・・・と繰り返して更に負債が重なっていくものだと思います。

これを解決するには評価の仕組みや組織自体の改善も重要だと考えます。

## 自分の会社の製品なのに自信を持って送り出せない

明らかにバグであったり、性能的に良くないものが見えている状態なのに(新規の案件ばかり優先されて)直すことができずにお客さんの所へリリースされていく製品を自信を持って送り出せるか？ 答えはNoです。

エンジニアとしてバグは極力ないものを、性能はなるべくいいものを送り出したい気持ちでやっていますが、
「今期の売上が足りないから別な案件を優先させなければいけない」「別な案件で人が足りないから別な案件に入ってほしい」と放置せざるを得ない状況になることは凄くストレスです。

そして実装内容を忘れた頃にお客さんからクレームが来て急ピッチで直さなければならず、残業しつつなんとか直したつもりが考慮不足でさらなるバグを生み出して・・・ということも何度か経験がありますが、これも精神衛生上よろしくは無いですし、結果として誰も幸せにならないです。

自分の会社で作っているシステムなので、自信を持ってお客さんのところへ送り出せるシステムにしていけるような組織にしていきたいですね。

# 今後気をつけていきたいこと(気をつけていること)

## コードを書く時間 <<<<<<<< コードを読む時間 ということを意識する

よく「コードをクリーンな状態に保つことが大切」というようなことを聞くことがありますが、なぜそれが大切か、レガシーなシステムの保守開発をしていくことで身にしみてわかりました。

既存の仕様を解析して理解するために、時として紙とペンを持ってメモを取りながらじっくり考えて、丸一日何もコードを書かずにコードを読んで居る時間のほうがはるかに多いです。

読む時間のほうが多いということは可能な限り可読性を高く保つ必要があると思います。

例えば以下のようなことに気をつけるだけでも大分違うと思います。

- インデントがずれている
    - → どこからどこまでがスコープなのかわかりにくい
    - インデントに使うものはTab/Spaceを統一することでエディタの設定等でズレないようにする。(スペースの場合はいくつなのか統一する。)
- 適当な位置で改行が挟まれておらず、横に長いコードになっている
    - →コードは基本的に縦にスクロールしながら読んで行きますが、画面外にはみ出る行が出てくるたびに横にスクロールしながら見るだけでも時間が掛かってしまう
- タイプミス・スペルミスに気をつける
    - →コードを読みながら「あれ？この単語ってこんな綴だったっけ・・・？」みたいな余計な脳のリソース消費を防ぐ
    - また、スネークケース、キャメルケース、ケバブケース等のルールを決めておく

また、「可読性が高い」＝「コードが短い 」というわけでは無いと考えています。
たまに、ワンライナー的に書いているコードもありますが、何をしているか解析するためにステップ実行して変数に何が入っているか確認するのに見にくくて余計な手間となってしまうこともあります。
変数に入れる必要がなくともわかりやすい名前を付けた変数に入れることで理解しやすくなるのであれば、多少コードが長くなっても理解に掛かる時間は少なくて済みます。(説明変数という考え方)

(実際にコードを読んでいて「こんな実装の仕方ができる自分すごいでしょ！」と言っているかのようなコードに出会うこともありますが、大抵読みにくくて理解するまでに時間がかかり、後のバグの原因になっていることが多いです。)

可読性を犠牲にして、メモリの使用量や実行速度的に誤差レベルしか変わらないような実装をするぐらいであれば、
多少の実行速度を犠牲にしてでも可読性を上げるべきだと考えています。
(0.1秒切り詰めるよりももっと大量に修正すべき箇所はあると思います。)

可読性の高いコードを書くためには、[リーダブルコード](https://www.oreilly.co.jp/books/9784873115658/) やQiitaや[Zenn](https://zenn.dev/)等の記事でも「こう書いていくほうがわかりやすい！」というような書き方が紹介されているのでそちらを参考にしています。

![picture_large978-4-87311-565-8.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127686/24740349-328f-adda-ba5e-8c62c4f8d0ec.jpeg)

<details>
<summary>ちょっと小話</summary>

サーバサイドをフレームワークなしのPHPで実装しているシステムで以下のような実装をしているものがありました。

user.php 

```php

// フロントからAjaxで受けたデータを処理
...
// 共通の初期処理等
...

if ($postData['method'] === 'get') {
    // ユーザー取得処理
    ...
} else if ($postData['method'] === 'list') {
    // ユーザー一覧取得処理
    ...
} else if ($postData['method'] === 'insert') {
    // insertの処理
} else if ($postData['method'] === 'update') {
    // updateの処理
}

// 共通の後処理等
...

// フロントへ返す処理
```

ものによりますが、各処理の`...`の箇所は数百行あることもありました。

このような実装をしていると何が問題か？パッと思いつくものでも以下のような問題があります。

- とにかく見通しが悪い
- VSCodeやPHPStorm、NetBeans...のようなIDEでアウトライン解析が効かず、その処理が行われている箇所までひたすらスクロールしたり、わざわざ検索したりする必要がある
- 変数のスコープも非常にわかりにくくなる
    - 10行目ぐらいで定義している変数が2000行目ぐらいで使われていたり・・・とかなりわかりにくいです
- Docを書きにくい
    - 処理それぞれが関数ではないのでPHPDocを書いたところでIDE側で認識しないので価値が薄くなってしまう
- ファーストリターンできない
    - (`goto`で無理やり後処理の箇所に飛ばしている実装もありましたが、あまり`goto`は使いたくは無いですね・・・)

Class化、関数化する改修をしましたが、変数の参照範囲がわかりにくいこともあり、改修にはかなり時間も掛けて苦労しました。
ですが、こういった部分をきれいにしていくことでその後のコードを読むスピードや改修を行うスピードははるかに良くなりました。
</details>


## ドキュメントを残す

10～15年前に始動したプロジェクトでドキュメントがロクに残されていないようなプロジェクトの保守・改修案件の担当になったとき、苦労したことは、既存仕様がわからない状態のソースと画面の動作を見ながら仕様の解析を行い、問い合わせの対応を行ったり、既存仕様と競合しないように追加機能を開発しなければならなかったことです。

仕様を残したドキュメントが残っていれば、そのドキュメントと照らし合わせながら動作を確認することでシステムの大枠を理解することができ、ソースを眺めながら理解するという無駄な作業を少なくすることができます。

また、問い合わせの内容やバグ・改修内容を記録しておくことも重要だと考えています。
お客さん側の担当者が変わって同じ問い合わせが来ることもあるかもしれません。そのときに再度調べ直すのでは時間がもったいないです。
改修内容を記録しておくことでどういった経緯でその改修を行ったかを把握することができます。

更に、急用等で他の人が一時的に対応せざるを得ない状況となったときに何もない状態から取り掛かるのと、ドキュメントが残された状態で取り掛かるのとでは掛かってしまう工数だけでなく、気持ちの問題的にも雲泥の差があります。


「アジャイルだからドキュメントは書かなくていい」という都合の良い解釈をするような人も居ますが、可能な限りドキュメントは残して行くべきだと思います。
直接的にバグを改修したり、性能改善を図るような改修は技術的負債の返済を行うことではありますが、ドキュメントを残すことは技術的負債を増やさないことに繋がると考えています。

以前にも投稿した↓の記事も読んでいただけますと幸いです。

https://qiita.com/g-iki/items/6d3fe4889769a6e03232

## 自動化できるところは自動化する

- 何度も行うようなことはスクリプト化する
- テストを自動化できるものは自動化する

といったことで面倒な作業に関わる時間を減らしていくことも技術的負債の返済につながると思います。

なにも全てを自動化しよう！と言っているわけではなく、できる範囲のことから自動化して、面倒な作業に関わる時間を減らすことで、浮いた時間で更に負債の返済を行う、ドキュメントが足りていない箇所の整理をする時間に充てる、もっと自動化できる部分があれば自動化を行うための時間を確保する。
といった負債を返済するための地盤を固めて行けると思います。

私の場合はGitLab CI/CDを使用してビルド作業を自動化したことで、ビルド作業自体にかける時間を減らせただけでなく、間違いが起きにくくなったことでそのリカバリーの時間も減らすことができました。
今後もGitLab CI/CD等のツールを活用してテストが自動化できるような仕組みにしていきたいと考えています。


# 終わりに

レガシーなシステムの保守は最初の方はすごく嫌でしたが、「最新のフレームワークを使って何かを作る！」というようなキラキラしたような体験よりも、今後エンジニアとして生きていく中で重要な考えを与えてくれたものだと思っています。
今後もこの経験を活かして頑張りたいと思います。



---

2023/01/10 追記:
コメントで頂いておりましたコードフォーマットの自動化に関して[GitLab CI/CD × php-cs-fixerを利用してPHPコードを自動フォーマットする](https://qiita.com/g-iki/items/9e8a31e03b893d06a268)で記事化してみました。
よろしければご参照ください。