ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

AI時代に生き残るコード:人間が読める言語の価値の再確認

I. 導入:創造の新たな日常語

ソフトウェア開発の世界は、今、根本的な変革の渦中にあります。「Vibecoding」という新たな潮流が、その中心にあります。これは、開発者が達成したいことを自然言語で記述し、実装の大部分をAIに委ねるという開発スタイルです 。Gemini Code AssistやGitHub Copilotといったツールがこのパラダイムを牽引し、反復的なタスクを自動化し、プログラミングへの参入障壁を下げることで、かつてないほどの生産性向上を約束しています 。

しかし、このAIによるコード生成の容易さと速度は、保守性、セキュリティ、そして最終的な説明責任という、長期的かつ新たな課題を生み出しています。AIはコードを「書く」ことはできますが、そのコードを理解し、デバッグし、進化させていく最終的な責任は人間にあります 。この文脈において、開発の焦点はコードを「書く」行為そのものから、後に残される「成果物」の品質へと移行します。

本稿の中心的な論点は、AI時代におけるソフトウェアシステムの真の価値は、その初期作成の速さではなく、長期的な回復力、すなわち人間のエンジニアによる理解可能性によって測られる、というものです。したがって、形式的で人間が読めるプログラミング言語は時代遅れになるどころか、むしろ明確性と厳密性の基盤として、その重要性をこれまで以上に増しているのです。

II. 自動化の甘い誘惑:AI生成コードの魅力と危険性

AIコーディングツールは、開発者に大きな利益をもたらす一方で、見過ごすことのできない深刻なリスクを内包しています。その性質を深く理解するためには、生産性の向上という魅力の裏に潜む危険性を、具体的なデータに基づいて冷静に分析する必要があります。

2.1 ブラックボックスのジレンマ:「動けば良い」では済まされない

AI生成コードがもたらす最も根源的な問題の一つは、それがしばしば「ブラックボックス」として機能することです。つまり、その内部ロジックや意思決定プロセスが、人間にとって不透明なのです 。これは、コードを生成する大規模言語モデル(LLM)の複雑な階層構造に起因する必然的な結果です 。多くの場合、生成されたコードの「なぜ」が欠落しており、開発者はその動作原理を完全には理解できません 。

この問題の深刻さは、他のハイステークスなAI応用分野、例えば医療診断や自動運転と比較することでより明確になります 。医師がその論理的根拠を理解せずにAIの診断を盲目的に信頼できないように、開発者もまた、そのロジックが不可解なシステムを責任もって保守することはできません。このようなシステムにおけるバグは、単なるエラーではなく、解決が困難、あるいは不可能な「謎」となり、将来の改善を妨げます 。この不透明性は、システムへの信頼を侵食し、障害発生時の説明責任を果たすことを不可能にします 。

実務的な保守の観点から見ると、ブラックボックス問題は、システムのデバッグリファクタリング、あるいは安全な機能拡張を著しく困難にします。あるコードが「なぜ」動くのかを説明できない開発者は、それが「なぜ」故障する可能性があるのか、あるいは変更がどのような影響を及ぼすのかを予測することもできません 。結果として、脆く、変化に抵抗するシステムが生まれてしまうのです。

2.2 欠陥の工場:セキュリティ欠損の定量

AIによるコード生成は、単なる品質の問題にとどまらず、深刻なセキュリティリスクを生み出す「工場」と化しています。最新の調査によれば、AIが生成したコードの45%にセキュリティ上の欠陥が含まれていることが明らかになっています 。これは些細な問題ではなく、システム的な脆弱性の大量生産に他なりません。さらに憂慮すべきは、モデルが構文的に正しいコードを生成する能力は向上しているにもかかわらず、そのセキュリティ性能は停滞したままであるという事実です 。

AIモデルが一貫して生み出す脆弱性のパターンは、具体的かつ深刻です。

クロスサイトスクリプティング(CWE-80): 驚くべきことに、安全なコードの生成に86%の確率で失敗します 。 ログ注入(CWE-117): 同様に問題が多く、88%のケースで失敗します 。 SQLインジェクション(CWE-89): 比較的良好な結果ではあるものの、依然として20%の失敗率を記録しており、重大なリスク要因です 。 暗号化の失敗(CWE-327): 14%のケースで安全でない実装が生成され、機密データ漏洩の危険性をはらんでいます 。 これらの脆弱性には、安全でないデフォルト設定、ハードコードされたシークレット、不十分なアクセス制御などが含まれることが多く、システムの防御を根底から揺るがします 。

では、なぜこのような事態が発生するのでしょうか。根本的な原因は二つあります。 第一に「トレーニングデータの汚染」です。AIモデルは、脆弱なコードを大量に含む公開リポジトリから学習します。その結果、安全でない実装も「有効な解決策」として学んでしまうのです 。

第二に「セキュリティコンテキストの欠如」です。AIはアプリケーションのアーキテクチャ、データフロー、ビジネスロジックを理解せずにコードを生成するため、機能的には正しくても、セキュリティ的には欠陥のあるコードを生み出してしまいます 。

さらに、このリスクは言語によっても大きく異なります。Javaは特にセキュリティ性能が低く、合格率はわずか29%です。これは、Javaがサーバーサイド言語として長い歴史を持ち、トレーニングデータに現代のセキュリティ意識が浸透する以前の古いパターンが多く含まれているためと考えられます 。

2.3 見えざる負債:技術的負債のエンジンとしてのAI

「技術的負債」とは、長期的でより優れたアプローチの代わりに、安易な短期的な解決策を選んだ結果として生じる、将来的な手直しのための暗黙的なコストを指します 。AIツールは、品質よりも速度と量を優先するように最適化されており 、「手軽な修正」や定型コードの生成に多用されることで、この技術的負債の蓄積を加速させます 。その結果、現時点では動作するものの、将来的に保守が困難なコードが量産され、システムの複雑化、保守性の低下、リファクタリングコストの増大を招きます 。

この「見えざる負債」が経済に与える影響は計り知れません。

米国における低品質なソフトウェアの総コストは2.41兆ドルに達し、そのうち蓄積された技術的負債は約1.52兆ドルと推定されています 。 エンジニアは、平均して業務時間の33%を技術的負債の対応に費やしています 。 組織は開発時間の23%から42%をこの負債のために浪費しており 、IT予算の平均30%がその管理に充てられています 。 具体的には、100万行のコードを持つプロジェクトにおける技術的負債の年間コストは30万6000ドルに相当し、5年間で150万ドルに達すると予測されています 。 AIによるコード生成がもたらす生産性の向上は、一見すると魅力的に映ります。しかし、その実態は単なる時間的な努力の移動に過ぎないことが多いのです。初期開発で節約された時間は、後のデバッグ、セキュリティパッチの適用、そしてリファクタリングの段階で、高い利子をつけて返済を迫られます。AIが生み出すコードは高い確率で欠陥を含み 、その修正には膨大な開発者の時間と予算が費やされる ため、初期の速度向上は、ライフサイクル全体で見れば相殺され、むしろマイナスに転じることさえあります。この「速度のパラドックス」は、AI導入のROIを評価する上で決して無視できない要素です。

さらに、AIは個別のバグを導入するだけでなく、組織の攻撃対象領域とセキュリティ負債を、かつてない速度で体系的に増大させます。人間が書いたコードであれば、その意図や責任の所在を特定の開発者にまで遡ることができますが、AIが生成したコードには明確な「作者」が存在しません 。この責任の曖昧さが「修正の複雑化」を招き、誰もが責任を感じないまま脆弱性が放置され、修正が遅れる原因となります 。結果として、脆弱なコードが組織全体で指数関数的に増加し、個々の問題の総和をはるかに超える複合的なリスクを生み出すのです。

そして、このプロセスは「スキルの空洞化」という負のスパイラルを引き起こします。不透明で低品質なコードを生成するツールに過度に依存することは、プロンプトの記述には長けていても、自らが構築しているシステムのデバッグ、最適化、あるいは根本的な理解ができない開発者を増やすリスクをはらんでいます 。このスキル低下は、問題を引き起こしたツールへのさらなる依存を促し、深いエンジニアリング知識の喪失へとつながる危険なフィードバックループを生み出します。

III. 「AIに言えばいい」という幻想:自然言語と曖昧性の深淵

AIによるコード生成の根底にあるのは、自然言語プログラミング(NLP)というコンセプトですが、ここにはソフトウェア開発の厳密性とは相容れない、根本的な矛盾が存在します。

3.1 曖昧さと厳密性の衝突

自然言語は、本質的に曖昧で、文脈に依存し、複数の解釈が可能です 。日常会話では、この柔軟性が円滑なコミュニケーションを可能にしますが、コンピュータシステムに指示を与える際には致命的な欠陥となり得ます。一方、プログラミング言語は、コンパイラインタプリタによる決定的で一意な解釈を保証するために、絶対的な厳密性をもって設計されています 。

自然言語プログラミングは、この根本的な性質の違いを無視し、「ユーザーログインシステムを作って」のような曖昧なプロンプトを許容します。しかし、この一文には、セキュリティ要件、データストレージの仕様、パスワードポリシー、エラーハンドリング、UI/UXといった、本来であれば厳密に定義されるべき無数の要素が欠落しています。AIはこれらの欠落した情報を、その学習データに基づいて「推測」で補完しますが、その推測がユーザーの真の意図と一致する保証はどこにもありません 。

3.2 最も重要な工程の省略

この曖昧さの許容は、ソフトウェア開発における最も重要な工程、すなわち「要件定義」と「アーキテクチャ設計」の意図的な省略を助長します 。従来の開発プロセスでは、これらの工程を通じて、関係者間の合意を形成し、システムの振る舞いを厳密に定義する仕様書が作成されます。この仕様書こそが、開発、テスト、保守のすべての工程における「信頼できる唯一の拠り所」となります。

しかし、自然言語プログラミングでは、この骨の折れる知的作業がスキップされ、動くものが即座に生成されてしまうため、「確定した仕様がどこにもない」という危険な状態が生まれます 。結果として出来上がるのは、砂上の楼閣のようなシステムです。予期せぬ振る舞いをした際に、その正誤を判断するための基準となるべき仕様が存在せず、あるのは元の曖昧なプロンプトだけです。これでは、保守作業はAIの「意図」をリバースエンジニアリングする当て推量のゲームと化してしまいます 。

このプロセスは、いわば「シュレーディンガーの仕様書」を生み出します。自然言語プログラミングで開発されたシステムの真の仕様は、ユーザーが「意図した」もの、AIが「解釈した」もの、そして生成されたコードが「実際に動作する」ものという、複数の状態が重なり合ったまま存在します。この仕様の重ね合わせ状態は、バグが発見され、ユーザーの意図とAIの解釈の間に齟齬があったことが明らかになった瞬間に、初めて一つの(そして多くの場合、誤った)状態に収束します。この時点では、拠り所となるべき「真実の源」はもはや存在しません。プロンプトはあまりに曖昧で、コードはその推測の産物でしかないため、バグ修正は現実を再交渉するプロセスとなってしまうのです。

一見すると、自然言語プログラミングはソフトウェア開発を民主化するように見えます。しかし、その実態はリスクの集中化に他なりません。専門家でない人々が、仕様定義や設計という規律を欠いたまま複雑なシステムの作成を開始することを可能にしてしまいます。これは、曖昧さを明確にし、アーキテクチャ上の欠陥を修正するという、目に見えない莫大なコストを、将来の保守チームに転嫁する行為に等しいのです。創造の「民主化」は、保守の「独裁化」へとつながります。

IV. 人間の成果物としてのコード:可読性と保守性の不変の原則

AIがもたらす課題に対する解決策は、AIを拒絶することではなく、人間の理解を最優先するプログラミング言語と開発文化を、これまで以上に重視することにあります。

4.1 本当の仕事は「読む」こと:理解の経済学 ソフトウェア開発の世界には、開発者はコードを書く時間の約10倍を読むことに費やすという、よく知られた経験則があります 。この事実を直視すれば、長期的な生産性を向上させるための最も効果的な手段が、コードの可読性を最適化することであるのは明らかです。

可読性の高いコードは、開発者の認知負荷を直接的に軽減し、システムの理解、デバッグ、拡張を迅速化します 。これは単なる美学的な選択ではなく、チームのパフォーマンス向上、バグの減少、保守コストの削減に直結する、極めて重要なビジネスおよびエンジニアリング指標なのです 。

Googleのような先進的な組織は、この原則を「Readability」レビューという形で制度化しています。これは、言語の専門家がコードレビューに参加し、一貫したスタイルと品質基準を組織全体で維持する仕組みです 。このような取り組みは、可読性を個人の好みから組織的な資産へと昇華させ、メンターシップと継続的な改善の文化を育みます 。

4.2 レガシーの文化:COBOLPerlから学ぶ不朽の教訓

ここで、COBOLを時代遅れの遺物としてではなく、驚異的な長寿命のケーススタディとして分析してみましょう。COBOLは今なお、世界の基幹インフラの膨大な部分を支えています。全銀行システムの43%、ATM取引の95%、そして日々の商取引額にして数兆ドルが、この言語の上で動いているのです 。

COBOLが生き永らえてきた理由は、その言語仕様の優雅さにあるのではありません。その周辺に構築された強固な「人間中心のシステム」にあります。数十年にわたって蓄積されたドメイン知識、確立された保守手順、そして(今や減少しつつある)専門家の存在が、その命脈を保ってきました 。その冗長で英語に近い構文は、ビジネスコンテキストにおける可読性を意図して設計されたものでした 。

しかし、COBOLは同時に警鐘も鳴らしています。モノリシックな複雑性、隠れた依存関係、そして深刻なスキル不足は、コードを支える「人間の文化」が崩壊し始めたときに、いかに甚大なリスクが生じるかを物語っています 。

一方でPerlは、その柔軟性が「書き捨て」のコードを生みやすいというパラドックスを抱えながらも、その強力さと組織内に蓄積された知識によって、今なお保守の現場で生き続けています 。

4.3 人間中心設計の現代的継承者:GoとRustの哲学

レガシー言語が直面する課題に対し、現代のプログラミング言語は、開発者の明確性と安全性そのものを設計思想の中心に据えることで応えています。

Go:機能としてのシンプルさ Go言語は、大規模システムを蝕む複雑性との戦いを明確な目標として設計されました。その哲学は、シンプルさ、小さな構文、そして高い可読性を最優先し、大規模で分散したチームが共有のコードベースを容易に保守できるようにすることにあります 。

Rust:契約としての安全性 Rustは、メモリ安全性とスレッド安全性を「コンパイル時」に保証するという、革命的なアプローチを提示します 。所有権、借用、ライフタイムといった概念は単なる機能ではなく、「壊れるコードは、そもそも書かせない」という哲学の具現化です 。

これらの言語は、人間のためのガードレールを意図的に構築するという設計思想を体現しています。構文的には正しくても、意味的・構造的に欠陥のあるコードを生成しがちなAIとは対照的に、GoやRustは、そもそも質の悪い、安全でない、あるいは読みにくいコードを書くこと自体を困難にするのです。

プログラミング言語の選択は、単なる技術的な決定ではなく、複雑性とリスクをいかに管理するかという哲学的な立場表明に他なりません。COBOLの哲学は「ビジネスデータの可読性」、Goの哲学は「シンプルさによる明確性」、そしてRustの哲学は「制約による正しさ」です。これらの哲学が、その言語で構築されたシステムの長期的な保守特性を決定づけます。一方で、現在のAIによるコード生成は、「プロンプトに合致する、もっともらしい出力を生成する」という確率論的なアプローチを超えた、一貫した設計哲学を欠いています。この指針の欠如こそが、その最大の長期的弱点なのです。

また、言語そのものが持つ特性だけでなく、それを運用する組織文化が、コードの可読性を支える「ランタイム」として機能します。GoogleのReadability制度 や、コードレビューを重視する文化 は、品質基準を強制する仕組みです。このような文化的な強制力がなければ、たとえ「読みやすい」とされる言語を使っても、保守不可能なコードは生まれてしまいます。COBOLの長寿命は、言語だけでなく、その周りに育まれた保守の文化によって支えられてきたのです。

V. 未来への航海:人間とAIエンジニアの共生フレームワーク

AIの台頭は、ソフトウェアエンジニアの役割を根本から再定義します。それは、人間のエンジニアを不要にするのではなく、より高度な知的作業へとシフトさせるのです。

5.1 エンジニアの進化する役割

ソフトウェアエンジニアの役割は、コードの主要な「作成者」から、システムの「アーキテクト、キュレーター、監査役」へと移行します 。最も価値のあるスキルは、もはや構文の習熟度ではなく、高レベルの問題解決能力、システム設計、批判的思考、そしてAIの生成物が正しさ、セキュリティ、効率性の観点から妥当であるかを検証する能力になります 。

未来の開発者は、AIエージェントを「指揮」し、AIが生成したコンポーネントを人間が設計したより大きなアーキテクチャに統合し、「コード中心」ではなく「インテリジェンス中心」の開発に注力することになるでしょう 。これには、ドメイン知識とビジネス目標への深い理解が不可欠です 。

5.2 意図の契約書としてのプログラミング言語

この新しいパラダイムにおいて、形式的なプログラミング言語の重要性は、低下するどころか、むしろ増大します。それは、人間の「意図」とAIによる「実行」との間に交わされる、曖昧さのない、法的に有効な「契約書」としての役割を担うからです。

人間は、RustやGoのような言語の厳密性を用いて、アーキテクチャ、インターフェース、安全性の制約、そして中核となるロジックを定義します。その上で、AIに実装の詳細を埋めるタスクを委任することができます。しかし、そのAIの作業は、コードで定義された厳格で検証可能な「契約」のルールに拘束されるのです。

この変化は、ジュニアレベルとシニアレベルのスキルギャップを劇的に拡大させる可能性があります。AIツールは、定型コードの作成や単純な関数の実装といった、従来ジュニア開発者が担ってきたタスクの自動化に長けています 。これにより、ジュニア開発者がシニアアーキテクトへと成長するために必要な基礎的な経験を積む機会が失われるかもしれません。この問題に対処しなければ、将来的にはシニアレベルの人材不足が生じる可能性があります 。

さらに、AIが生成するコードの量が増大するにつれて、実行時テストや手動レビューによる品質保証は、コストと信頼性の面で持続不可能になります。戦略的な優位性は、可能な限り早い段階で正しさを検証できるエコシステムへと移行するでしょう。Rustが提供する強力なコンパイル時保証は、この未来の青写真です。究極の目標は、ビジネスロジックセキュリティポリシーアーキテクチャ上の制約を、AIが生成したコードに対して静的に分析・検証できるほど厳密に定義できるようにすることです。これにより、コンパイラや関連ツールが、コードが実行される前にその正しさを証明する、最終的な品質ゲートキーパーとしての役割を果たすことになるのです。

VI. 結論:自動化の時代における職人技の保存

本稿で展開してきた議論の核心は、AIによるコード生成がもたらす即時的な生産性の向上は魅力的であるものの、セキュリティ、保守性、品質の面で深刻な長期的コストを伴う、という点に集約されます。解決策はAIを拒絶することではなく、人間中心の厳格なエンジニアリング規律の枠組みの中で、その力を活用することです。

組織が構築できる最も価値のある技術的資産とは、単に機能するシステムではなく、理解可能で、検証可能で、進化可能なシステムです。これこそが、「人間が読めるコードと、後世に残せる知見」の本質です。そして、形式的なプログラミング言語は、この資産を創造するための我々の最も重要な道具であり続けます。

誰でもコードを「生成」できる時代において、真の差別化要因となるのは、持続可能なシステムを「設計」する能力です。本稿の最終的なメッセージは、ソフトウェアの職人技(クラフトマンシップ)の原則に再投資すべきだという、業界への呼びかけです。未来は、最も速くコードを書く者ではなく、最も明確に思考する者に属するのです。

心理的安全性から具体的な改善アクションまでを行う、一開発チームの振り返り方法を公開

Contractsの倉島です。

アジャイル開発を行っているチームにとって、「振り返り(レトロスペクティブ)」は学びと成長のための重要なイベントです。でも、「いざやってみると、ただの雑談で終わってしまった」「具体的な改善に繋がらない」なんて悩みを抱えているチームも多いのではないでしょうか?

そこで今回は、私が所属している開発チームが実際に行っている振り返りの様子を、実際のログを基にご紹介します。

Step 1:本題の前に心をほぐす「チェックイン」

私たちの振り返りは、いきなり本題から始まりません。まずは「チェックイン」と呼ばれる時間で、全員が安心して話せる雰囲気を作ります。

今回のテーマは「一押しのアイスは何ですか?

倉島:「僕はもう鉄板ですけど『チョコモナカジャンボ』ですね。味が良くて、食感が良くて、手が汚れない。完璧です。」

Sさん:「なるほど!僕は量ですね。値段の割に一番量が多い『スーパーカップ』が最高です。昔、明治製菓に『かぼちゃ味を出してください!』ってハガキを送ったことがあるくらい好きで…」

Nさん:「僕はガーナの『チョコ&クッキーサンド』ですね。あとサーティワンの『ポッピングシャワー』も好きです。口の中でパチパチするやつ。」

倉島:「え、あれあんまり美味しくなかったですけど…口の中に違和感が残る感じで…」

こんな風に一見するとただのアイス談義ですが、仕事の話から離れてお互いの好みや人となりを知ることで、チーム内に心理的安全性が生まれます。「こんなこと言っても大丈夫かな?」という不安がなくなり、この後の振り返りで踏み込んだ意見も出しやすくなります。

Step 2:感情から課題までを可視化する「温度計の振り返り」

チェックインで場が温まったら、本題(振り返り)を行います。普段、その時に応じて色々な振り返り手法を利用していますが、今回は「温度計の振り返り」という手法を使っています。

これは、感情や出来事をカテゴリーに分けて、下から順番に付箋(ふせん)に書き出していく手法です。

  • やり方
    1. 各項目について2分間、全員で一斉に黙々と付箋に書き出す。
    2. 全員が書き終わったら、順番に内容を共有していく。
  • 温度計の5つの項目
    1. 感謝 (Gratitude):スプリント中の「ありがとう」を伝え合う。
    2. 気づき・学び (Insights/Learnings):新しく学んだことや、ハッとしたこと。
    3. よく分からなかったこと・興味のあること (Puzzles/Interests):疑問に思ったことや、もっと知りたいこと。
    4. 提案付きの苦情 (Complaints with Suggestions):課題と、それに対する改善案。
    5. 希望と願望 (Hopes/Wishes):「こうなったらもっと良くなるのに!」という未来への期待。

下から上(感謝→願望)へと進むことで、ポジティブな雰囲気から始め、徐々に課題解決へとスムーズに移行できるのが特徴です。

温度計振り返り

Step 3:課題を「具体的なアクション」に変えるプロセス

付箋を出して共有するだけだと、浅く議論して改善につながるアクションを生み出しづらいです。そこで、出てきた課題の中から、投票で特に重要なものを2つほど選び、その場で具体的な解決策を議論します。

今回のテーマ:「Gitのマージ忘れを防ぎたい」

今回は「リリース直前のGitのマージ忘れがヒヤッとした」という課題が選ばれました。

倉島:「人間は忘れる生き物なので、チェックリストみたいなアナログな方法じゃなくて、プロセスやシステムで解決したいですね」

Nさん:「同意ですが、そのための仕組みを作るのも大きくコストがかかりそうですね」

Sさん:「じゃあ、まず一番手軽に試せることからやってみませんか?スプリントの終わりに、全員で5分だけ集まってリリース対象のマージを確認する時間を設けるのはどうでしょう。」

このような会話を経て、一つの「Try(試してみること)」を決めました。

【決定したアクション】毎週火曜日の14:30から5分間、「マージ確認会」を全員で実施する。

このように、漠然とした「忘れないようにしたい」という願望を、「誰が」「いつ」「何をするか」という具体的なアクションアイテムに落とし込むことで、チームは着実に改善を積み重ねていくことができます。 また、Tryを実施した後に効果があるかの確認を次回以降の振り返りで行い、必要なアクションだけを残す行為も重要です。この際、可能であれば「何が」「いつまでに」「どうなっていれば」効果があったと言えるか、を定めておくとより効果的な振り返りができると思います。

まとめ

私たちの振り返りの様子、いかがでしたでしょうか?

  • ① チェックインで心理的安全性を確保する
  • フレームワークを使って網羅的に意見を出し合う
  • ③ 投票と議論を通じて、重要な課題を絞り込み、具体的な改善アクションに繋げる

この3つのステップを踏むことで、チームは毎週少しずつ強くなっています。 この記事が、皆さんのチームの振り返りをより良くするためのヒントになれば嬉しいです!

【転職後のリアルな3ヶ月】AIも活用!未経験ドメインで僕が実践したキャッチアップ術

こんにちは! Contractsに入社してから、気づけば3ヶ月が経ちました。㓛刀です!本当にあっという間です。 現在は主に新規機能開発のチームに所属していて、日々新しいことばかりで刺激的な毎日を送っています。

最初の頃は、正直なところ会議で意見を言うことすら難しく、「何がわからないのかすら、わからない」状態でした。 ですが、最近ようやくその段階を抜け出せてきたかなぁと感じています。

そこで今回は、この3ヶ月で僕自身がどのようにプロダクトのキャッチアップを進めてきたか、その具体的な方法をまとめてみたいと思います。 これから新しくチームに加わる方や、同じように新しい環境で頑張っている誰かの参考になれば嬉しいです。

Step 1:準備運動編 - コードを読む前に、まずやるべきこと

新しい環境に入って、すぐにコードを読みたくなる気持ちはすごく分かります。 でも、ちょっと待ってください。装備も整えずにいきなりラスボスに挑むようなものなので、まずはしっかり準備運動から始めましょう。

✅ オンボーディング資料を徹底的に読み込む

会社が用意してくれている資料は、いわば「公式ガイドブック」。ここに書かれていることは、今後の冒険の基礎になります。

  • 開発環境の構築手順書: まずは自分のPCでアプリケーションを動かせなければ始まりません。手順通りに進めて、しっかり動く環境を作りましょう。
  • アーキテクチャ図: システム全体の地図です。自分がこれから関わる部分が、全体の中でどんな役割を担っているのかを把握するだけでも、今後の理解度が大きく変わってきます。
  • コーディング規約: チームで開発を進める上での共通ルール。これを読んでおくだけで、後のレビューがぐっとスムーズになります。

ぶっちゃけ存在しないものがあり、誰かの頭の中にしかないというのも現実です。

✅ コミュニケーションツールを巡回して「空気」を掴む

Contractsで言えばSlackやGitlab、Notionは、チームの文化や過去の意思決定の歴史が詰まっています。

  • 過去のプルリクエストを眺める: どんな修正が行われ、どのようなレビューがされているかを見ることで、「こういうコードが評価されるんだな」という感覚が掴めます。
  • Wiki(Confluenceなど)を読む: 議事録や設計書に目を通すと、「なぜこの機能が生まれたのか」「なぜこの技術が選ばれたのか」といった背景を知ることができ、プロダクトへの理解が深まります。

Step 2:実践編 - 「森」と「木」をバランス良く理解する

準備ができたら、いよいよ実践です。「プロダクト全体の動き(森)」と「個々のコード(木)」を、効率よく見ていきましょう。

🌳 アプローチ①:ユーザー視点をインストールして「森」を見る

まず僕がやったのは、「ユーザーとしてプロダクトを徹底的に触ってみる」ことでした。

▼具体的な進め方 ヘルプサイトなどを参考に主要な機能を操作しつつ、ブラウザの開発者ツールを開いて裏側の動きを観察します。 「このボタンを押すと、こういうAPIリクエストが送られて、データベースのこのテーブルが更新されるんだな」 というように、ユーザーの操作とシステムのデータの流れを結びつけることを意識していました。これをやっておくと、後々の開発がぐっと楽になります。

🌲 アプローチ②:AIとツールを駆使して「木」を読み解く

全体像を掴んだら、いよいよ個別のタスクを通じてコードレベルの理解を深めていきます。ここでは、便利なツールをいかに使いこなすかが鍵になります。

1. 効果的なコードリーディングの小ワザ

  • git blameで「歴史」と「担当者」を探る: エディタのgit blame機能は、コードの各行が「いつ、誰によって書かれたか」を教えてくれます。複雑な実装の意図がわからない時、その背景を知る大きな手がかりになりますし、詳しい人に質問するきっかけにもなります。
  • デバッガで処理の流れを追う: コードを読むだけでは理解が難しい複雑なロジックは、デバッガを使って一行ずつ処理を追いかけるのが効果的です。変数の値がどう変わっていくかを実際に見ることで、理解度が格段に上がります。

2. 強力な相棒になってくれるAI(Amazon Qなど)

最近では、Amazon QのようなAIコーディング支援ツールが、キャッチアップの強力な相棒になってくれます。 まるで、頼れる先輩が隣でサポートしてくれているような感覚です。

  • 複雑なコードの解説: 理解が難しいコードをAIに「このコードは何をしているの?」と聞けば、自然な言葉で処理内容を説明してくれます。
  • 人に聞く前の「一次質問」相手として: 「こんな初歩的なことを聞いても大丈夫かな…」と不安な時でも、AIなら気兼ねなく質問できます。まずAIに聞いてみて、それでも分からなければ人に聞く、というステップを踏むことで、質問の質も自然と上がりました。

3. 「高速学習サイクル」を回す意識 最終的には、**「まずは取り組む → 壁にぶつかる → 調べる/質問する → 理解して進める」**というサイクルを、いかに速く回せるかが成長の鍵だと感じています。


Step 3:マインドセット編 - 焦らず、自分のペースで進むために

新しい環境では、周りのレベルの高さに焦ったり、自分の出来なさに落ち込んだりすることもあると思います。 僕も8月に腰の骨を折ってしまい、椅子に座ることすら辛い時期は、心身ともにかなりきついものがありました。 9月現在も完治しておらず、辛い日々を送っています。本当はもっと記事を書いたりもしたかった・・・

そんな時に、僕が大事にしていた考え方を少しだけ共有させてください。

  • 完璧を目指さない、まずは60点でOK: 転職してすぐに100点満点のパフォーマンスを出すのは不可能です。まずは環境に慣れ、小さなことからチームに貢献していく。その積み重ねが大事だと思います。
  • 「できないこと」は「伸びしろ」: わからないことが多いのは、それだけ毎日新しいことを学べている証拠です。昨日より少しでも分かったことがあれば、それは大きな成長です。
  • どんな小さな成果も、自分で認めてあげる: 小さなバグ修正、ドキュメントの修正。どんなタスクでも、プロダクトを良くした立派な貢献です。一つ一つのタスク完了を、自分の自信に繋げていきましょう。

さいごに

色々とお話ししましたが、僕がこの3ヶ月で実践してきたのは、特別なことではありません。 ありきたりなことですし、誰かの当たり前のことかもしれません。 しっかり準備して、ユーザーの視点を持ち、便利なツールを使いこなし、前向きな気持ちでタスクに取り組む。 基本的には、この繰り返しでした。

この記事が、新しい環境で頑張るあなたの、ちょっとした「回復薬」のようになれば幸いです。 最後まで読んでいただき、ありがとうございました!

コスト分析の「トイル」を撲滅!Amazon Q DeveloperのSREチームでの利用の現在地

  • はじめに
  • コスト分析の「泥臭い現実」
  • Amazon Q Developerによるコスト分析革命
    • 導入のきっかけ
    • 分析プロセスの全体像
    • AI分析の心臓部:プロンプトエンジニアリング
    • 月次分析が「3時間」から「10分」へ
    • 実際の分析結果
  • AI導入前の地道な取り組み
    • 第一歩:まずは「見える化」と「小さな成功体験」
    • 第二歩:地道な最適化と、見えてきた「文化の壁」
  • 今後の展望
  • まとめ
  • 参考にした資料や書籍

はじめに

こんにちは! ContractSでSREをしている福嶌(id:s-fukushima29)です。2023年4月に入社してはや3年目になりました。

自分の経歴としてはプログラマーとしてキャリアをスタートしました。その後、ContractSに入社する前はMSP(マネージドサービスプロバイダ)事業に携わり、日々お客様のAmazon Web Services (AWS)、Google Cloud、Microsoft Azureといったクラウド環境と向き合ってきました。
そこでは、1円のコストがビジネスにどれだけ影響するかを肌で感じていて、「スポンサーが石油王でもない限り、予算は有限だ」という現実を常に意識していました。そのため、「クラウド費用の内訳を分析して把握し、お客様に説明責任を果たすこと」は、お客様の事業成長を支える重要な責務だと考えています。
この考えは、自社サービスにおいても同様に重要です。コストの内訳を説明できなければ、事業環境の変化によるコスト削減の波が来たときに、その価値を正しく示すことが難しくなり削減の対象となってしまうからです。

ContractSに入社して感じたのは、プロダクト開発のスピードが最優先される文化の中で、インフラを見るSREである我々の"コストに対する意識が薄い"ということでした。
もちろん、それは成長するSaaS企業にとって健全な姿の一面でもありますが、MSP出身の自分には、それが「大きな改善のチャンス」であり、チームとして「伸ばすべき能力」だと感じられました。

この記事では、そんな「コスト意識が薄い」状態から、チームでコスト分析と最適化を当たり前にするようになり、Amazon Q Developerという強力なアシスタントを得て分析業務を劇的に効率化した道のりとこれからについてご紹介します。 同じような課題を抱えるSREや開発チームの皆さんにとって、少しでもヒントになれば嬉しいです。

続きを読む

(Java)QueryDslを使用して重複除去(DISTINCT)とソート(ORDER BY)を両立させる

ContractSの倉島です。
最近、DBへのアクセスを型安全にすべくQueryDslを用いているのですが困ったことに直面したので備忘録的に解決手段を記しておきます。

何に困ったか

とあるデータを一覧で取得する際に、「レコードの重複除去を行いつつ取得していない値でソートを行う」必要が出てきました。
しかし、ただ単純にDISTINCTとORDER BYを同時に指定するだけでは順番が狂って取得されてしまいました。

何が原因か

DISTINCTは内部でソートしてから重複除去を行うらしく同時に指定した場合は、DISTINCTが実行された後にORDER BYが実行されてしまうので狂った並び順でソートされてしまうと原因づけました。

解決策

MySQLのクエリだったら、親のテーブルの重複除去を行った結果を副問い合わせとしてそれをソート対象の値があるテーブルと結合すれば、DISTINCTとORDER BYは別々で実行されるので正しく取得できるかと思います。
ただ、QueryDslには副問い合わせの結果をjoinする機能は存在しない(もし存在していたらすみません!)ので、この解決策は使えません。
そこで考えたのが、ORDER BYに指定する値を副問い合わせ結果にしてしまうという策です。

具体的には

修正前(これだと狂った並び順で取得される)

              queryFactory
                .select(Projections.constructor(
                        SearchedResultEntity.class,
                        qMainEntity.mainId,
                        qMainEntity.xxx,
                        qSubEntity.subId,
                        qSubEntity.yyy
                ))
                .distinct()
                .from(qMainEntity)
                .innerJoin(qSubEntity)
                .on(qSubEntity.mainId.eq(qMainEntity.mainId))
                .leftJoin(qSubSubEntity)
                .on(qSubSubEntity.subId.eq(qSubEntity.subId))
                .orderBy(qSubSubEntity.property.asc());

修正後(これで正しい順番で取得される)

              SubQueryExpression<Long> subQuery =
                JPAExpressions.select(qSubSubEntity.value).from(qSubSubEntity)
                        .where(qSubSubEntity.propertyId.eq('sortPropertyId'),
                                qSubSubEntity.subId.eq(qContractHistoryEntity.subId));

              OrderSpecifier<Long> sort = Expressions.asNumber(subQuery).asc()

              queryFactory
                .select(Projections.constructor(
                        SearchedResultEntity.class,
                        qMainEntity.mainId,
                        qMainEntity.xxx,
                        qSubEntity.subId,
                        qSubEntity.yyy
                ))
                .distinct()
                .from(qMainEntity)
                .innerJoin(qSubEntity)
                .on(qSubEntity.mainId.eq(qMainEntity.mainId))
                .leftJoin(qSubSubEntity)
                .on(qSubSubEntity.subId.eq(qSubEntity.subId))
                .orderBy(sort);

取得する際はfetch()の実行もお忘れなく。
また、副問い合わせを使用する関係上、扱うデータが大量だとパフォーマンスに対する懸念が生まれるのでご注意ください。

最後に

クエリ文を直接記述する方法なら、最初の「親テーブルを重複除去した結果」を結合してしまえば解決していたことを考えると、今回の問題に限って言えばQueryDslは少し不便かもしれないですね。
しかしながらそれを補って余りある型安全やメソッドチェーンによる可読性の向上があると考えているので今後もQueryDslを有効活用できればと思います。

複数フィールドのバリデーションエラーを集約して表示するVeeValidate v4の活用法

こんにちは。ContractSでフロントエンドエンジニアをしている北原です。

弊社では、UIのバリデーションとしてVeeValidate v4を採用しています。 この記事では、複数のインプットを1つのバリデーションフィールドとして扱う方法について解決策を提示したいと思います。

目次

前提知識

今回の記事の前提知識です。VeeValidate v4についてご存知の方はスルーしてください。

VeeValidateとは?

Vue.jsにおけるフォームバリデーションを簡素化する人気のライブラリです。
日本語ドキュメントも充実しており、大変扱いやすいものになっています。 vee-validate.logaretm.com

VeeValidate v4 の Composition関数

1. useField

const { value, errorMessage } = useField(() => 
  'email',
  yup.string().email().required(),
)

useFieldは、コンポーネントをフィールドとして扱うことができる関数です。

v-modelにbindして使う実際の入力値valueや、バリデーションルールにそぐわない場合に生成されるerrorMeesageなどを有しています。 vee-validate.logaretm.com

2. useForm

const { handleSubmit } = useForm()

useFormは、記述したコンポーネントをフォームとして扱うことができる関数です。

前述したuseFieldを有するコンポーネントを子コンポーネント以下に持つ場合に用いることで、その値を取得して送信できるhandleSubmitなどを有しています。 vee-validate.logaretm.com

なぜ必要なのか?

弊社のプロダクトでは、契約書を管理するに当たって契約項目と呼ばれる関連情報を、各契約書が有しています。
それらをユーザの方へ入力していただく場合に必須項目としたいパターンが存在します。

このとき、以下のような別々のフィールドを、エラーメッセージを1箇所に集約してバリデーションをしたいパターンが出てきました。

エラーメッセージを1箇所に集約したい例

このフィールドは、契約書に対する自動更新機能を有効にしたとき、契約書の契約期間終了後に新しく引き延ばす契約期間の長さを入力するフィールドです。
左側は値(半角数字)のインプット、右側は単位(日、ヶ月、年から選択)のプルダウンとなっており、2つで1つです。

しかしVeeValidateの仕様上、このようなパターンでは左右それぞれのフィールドが、個々のバリデーションを持つ別のフィールド値として扱われてしまうため、上の画像のようにエラーメッセージの表示箇所を集約できません。

なんとか解決する手段を考えた結果がこの記事になります。

TL;DR

  1. エラーメッセージを自身の管理対象として加える関数をprovideするラッパーコンポーネントを作成
  2. useFieldを持つコンポーネントが、1. のコンポーネントからprovideされた関数をinjectしてエラーメッセージを管理対象に加える

以上の流れで実現します。

やってみる

参考

VeeValidate v4開発者、Abdelrahman AwadさんのCodeSandboxにあった以下の内容を参考にしました。

https://codesandbox.io/p/sandbox/handling-nested-forms-in-vee-validate-v4-p45up?file=%2Fsrc%2Fcomponents%2FChildForm.vue%3A30%2C3-53%2C3

実装例

1. ラッパーコンポーネントの作成

まず以下のように、動的にuseFieldの値を複数管理するための関数を提供するラッパーコンポーネントを実装します。

script

// Types
interface Injection {
  errorMessage: Ref<string>
  // 他に管理したい対象があれば追加する
}
export type ProvideAddField = (injection: Injection) => void

// Variables
const errorMessages = ref<Ref<string>[]>([])

// Computed
const allErrorMessages = computed<string[]>(() => (errorMessages.value.map(it => it.value).filter(it => !!it)))

/**
 * エラーメッセージを登録する関数をprovideする
 */
onBeforeMount(() => {
  provide(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE,
    (injection: Injection) => {
      errorMessages.value.push(injection.errorMessage)
    },
  )
})

onBeforeMountのライフサイクルで、このコンポーネントへエラーメッセージを登録する関数をprovideしておき、それによって追加されるエラーメッセージ群のerrorMessagesも持っておきます。

また、実際にエラーが発生しているものだけに絞り込んだallErrorMessagesも合わせて持っておきます。

template

<template>
  <div>
    <slot />
    <!-- 複数のメッセージを表示したい場合はv-forすれば良い -->
    <AtomsErrorMessage v-show="allErrorMessages.length !== 0" class="mt-1" :message="allErrorMessages[0]" />
  </div>
</template>

template部には、provideした関数により追加されたエラーメッセージの表示箇所と、当該コンポーネントへエラーメッセージを追加したいフィールドコンポーネントを入れ込むためのslotを記述します。

2. フィールドコンポーネントから関数をinject

script

const { value, errorMessage } = useField(() => 
  'email',
  yup.string().email().required(),
)

// (中略)

/**
 * エラーメッセージを、ラッパーコンポーネントの管理対象として追加します ※
 */
const includeField = (errorMessage: Ref<string>) => {
  const injection: ProvideAddField | undefined = inject(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE)
  if (injection) injection({ errorMessage })
}

onMounted(() => {
  includeField('email', errorMessage)
})

フィールドコンポーネントへ、onBeforeMountedprovideされている関数を、次のonMountedライフサイクルで呼出す処理を追加しておきます。

※ ここでは便宜上素で定義していますが、composablesにしておくのが良いです

3. 実際に使ってみる

ラッパーコンポーネントのslotへ、実際にバリデーションしたいフィールドコンポーネントを入れこみます。

これにより、エラーが発生された場合に1箇所へ集約して表示できるようになります。

テンプレート記述例

まとめ

実装自体に手間はかかりますが、一度仕組み化してしまえば汎用的に利用できるため利便性は高いと思います。
もう少し簡素に実現できる方法がありましたら、ぜひご教授ください。

今回の記事が、少しでも同じような課題を抱えている方の助けになれば幸いです。

最後に

お読みいただきありがとうございました。
ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

APIファーストで契約管理システムの連携力を高める

こんにちは。ContractSでバックエンドエンジニアをしている毛見です。
弊社がAPIファースト開発を取り入れた背景や利点、実際のアプローチを整理してブログにしました。

契約ライフサイクル管理 (CLM: Contract Lifecycle Management) は、単なる電子契約、契約書の保管という枠を超え、営業、法務、財務、購買などの多様な業務との連携によって業務の統制や効率化を図ります。昨今の企業では、業務ごとに専用のSaaSや基幹システムを導入しており、これらのシステムとシームレスに連携することがCLMに求められています。
私たちは、この連携を柔軟に実現するために APIファーストのアプローチを採用しています。この記事では、その利点や実践内容、得られる効果についてご紹介します。

APIファーストとは?

APIファーストとは、システムやサービスの設計・開発において 「まずAPIを定義することから始めるアプローチ」 です。このアプローチでは、APIを単なる技術的なインターフェースではなく、システム全体の基本構造として捉えます。
参考: Postman What is API-first?

APIファーストの利点

利用者視点のインターフェース設計

APIの定義を先に行うことで、フロントエンド、他システム、外部の開発者の視点を意識した設計が可能になります。それにより他システムやパートナーとの連携が迅速で容易になります。

ノーコード/ローコードでシステム構築ができる世界を実現する

APIの定義において、RESTやOpenAPI(Swagger)などの業界標準を採用することで、ノーコード/ローコードでの連携が可能になり、外部システムやSaaSとの連携がスムーズになります。

柔軟なユースケース対応

ビジネス要件に応じてAPIを再利用・組み合わせることで、多様なユースケースに柔軟に対応できます。新しい製品やサービスを迅速に立ち上げる際にも、APIファーストの設計は大きな強みとなります。

APIファーストをどのように取り入れているか?

設計フェーズと実装フェーズ

APIの仕様書を作る方法として、実装後にコントローラーから生成する方法があります。それに対し、APIファーストの開発では、まず設計フェーズでAPI仕様をOpenAPIで記載します。記載したAPI仕様について開発者、プロダクトオーナーで合意し、テストケースを作成します。この時点で、外部サービスとの連携、既存のWebサービスで利用できるAPIとなるように検討を済ませます。その後に実装フェーズに移ります。

既存のWebアプリケーション用のバックエンドからストラングラーフィグパターンで移行する

ContractS ではAPIファーストを取り入れる以前のWebアプリケーション用のバックエンドサービスが存在します。そのバックエンドサービスには仕様が蓄積されていますが、外部サービスとの連携を考慮していないため、APIとして公開できません。そのため、公開も可能なAPIサービスとして作り直す必要があります。そこで、機能ごとにストラングラーフィグパターンで新たなAPIに移行をしました。既存のバックエンドサービスは移行先のAPIを呼ぶようにし、BFF(backend for frontend)として振る舞うことで、フロントエンドへの影響を最小限にとどめながら移行が可能になります。

テストと品質保証の自動化

実現の途中ではありますが、CI/CDによってAPIのE2Eテストを自動化し、相互運用性の担保を行います。これによりWebアプリケーションだけでなく、連携先のAPIに対しても品質を保証することができます。

ビジネス的な効果

顧客への導入提案において、Webアプリケーションだけでは実現が難しい要求に対し、API連携を利用した実現方法を提案できるため、「APIを使ってできます」と言えるようになります。 また、他システムとの連携について詰めていく中で、想定外の要件がでることもあるかと思います。そういった時に、特定のユースケースのためのAPIではなく、汎用的なAPIとして設計してあれば、それを組み合わせるだけで追加開発の必要なく要件に対応することができます。

最後に

契約ライフサイクルは、さまざまなシステムとの連携なしには成り立ちません。そのため、私たちは APIを核に柔軟な連携基盤を構築しています。これにより、顧客の多様な業務要件に応え、契約管理の効率化を推進しています。
実現途中のAPIのE2Eテストについてもブログ化を検討していますので、楽しみにしていただければと思います。

ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp