大規模研究開発PJのSystem Integrationチームに加入して実施した施策 前編

2024年12月20日掲載

キービジュアル

ソフトバンクアドベントカレンダーは20日目の記事です。

皆さんこんにちは。データ基盤戦略本部の牧野 颯太です。私は普段、分散コンピューティング基盤の研究開発プロジェクトにおいて、研究成果を統合するWebアプリケーション/ソフトウェアの開発に携わっています。また、このプロジェクトは、NEDO公募を通じて採択された研究開発テーマの中で実施されています。


研究開発段階のアプリケーション開発には、プロダクションのアプリケーション開発にはない独特な課題が存在します。

本記事では、大規模な研究開発PJのSystem Integrationチームに加入した際に、開発チームのリーダーとして3ヶ月間で実施した施策を前後編の2部作で紹介します。

前編では、研究開発段階のアプリケーション開発の課題を説明し、後編ではその課題にどのようにアプローチしてどのような効果があったかを紹介します。

目次

  • 大規模な研究開発PJのSystem Integrationチームに加入した際に、開発チームのリーダーとして3ヶ月間で実施した施策を前後編の2部作で紹介します。
  • 前編では、研究開発段階のアプリケーション開発の課題を説明します。
  • 同じような立場の方の参考になれば幸いです

プロジェクトの体制と特性

まずは軽く私の所属しているプロジェクトについて紹介したいと思います。先にお伝えしたように私は研究開発のPJに所属しているのですが、プロジェクトのほとんどは研究を進めるチームで構成されており、そこで生まれた成果物を顧客やステークホルダー向けに提供できるようにアプリケーション化する、というのが我々インテグレーションチームのミッションになります。

図1

大規模研究開発、と銘打ちましたがイメージ画像で示しているように、System Integrationチームの主な責務としては一般的なWebアプリケーションを作ることです。

私の所属PJではマイクロサービスアーキテクチャを採用しており、System Integrationチームのソフトウェアと各研究チームの開発したソフトウェアの関係は以下の図のような形をとります。

図2

本プロジェクトが通常の商用サービス開発と異なる点として、大きく2点あると考えています。

まずは研究開発段階であるという点です。そのため、ソフトウェア本体だけでなく、IFにも頻繁に変更が走ります。

もう1点は、OSSベースで開発を行い要件にマッチしない場合はクイックにソフトウェアを入れ替える事があるという点です。

以上の2点から開発するソフトウェアは変更容易性が高いことがとても重要になります。 

System Integrationチーム加入直後の状況と課題

私がSystem Integrationチームに加入した約半年前、System Integrationチームは編成されたばかりのチームということもあり、次のような課題がありました。

  1. テスタビリティが低かった
  2. 作業の並行性が低かった

それぞれについて詳しく話します

課題1:テスタビリティが低かった

まずは、1の「テスタビリティが低かった」についてお話しします。

ここで言う「テスタビリティ」とは、テストの実装のしやすさやテストの実行容易性の事を指しています。

当時のSystem Integrationチームのソースコードは、目的とする機能を実現することが第一になっており、保守しながら運用するには少々辛いコードになっていました。

具体的には、以下のようなイメージです。

図3

我々はサーバーサイドのアプリケーションをGoで書いているのですが、handlerの中に殆どすべてのロジックが記載されており、一枚岩とも言える大きな関数がいくつか存在する、というような状態でした。

その結果ユニットテストのテストケースは膨大になり、現実的には開発者の裁量によってテストが実装されるといったような課題がありました。

課題2:作業の並行性が低かった

前述のようなアーキテクチャで開発を開始した結果、タスクのチケットの切り方がほぼ機能単位になってしまい、分担して取り組むべき場面でも、メンバーが並行して実装に入りづらい状況が発生していました。

System Integrationチームは研究項目以外のほとんど全てを用意する責務を持つチームということもあり、人員の追加が予定されていたのですが、先に記述しようなアーキテクチャでは、大量のオーバーヘッドやコンフリクト(作業レベルでもソースコードレベルでも)が発生することは必至でした。

なぜこのような課題が発生していたのか?

課題1「テスタビリティが低かった」について

このような課題が発生していた理由は、一言で言うと、研究開発PJにおいては「品質は要件として最重要ではなかった」からです。

私たちが取り組んでいるNEDOプロジェクトでは、通常の製品開発とは異なる優先事項があります。それは、「研究開発した技術の有効性を検証すること」 を最優先にするという点です。

一般的な製品開発では、完成した製品の「品質」や「安定性」が重要視されます。市場に投入される以上、エンドユーザーが安心して使用できるレベルの品質が求められるのは当然のことかと思います。

しかしながら、本プロジェクトのような研究開発PJ、特に明確に締め切りが決まっているようなタイプのPJでは、まずは機能を実現すること、そして、その技術が有効であることを求められ、通常の商用開発との間で最もギャップを生んでいる点でした。

このことは、他のNEDOプロジェクトや一般的な企業の研究開発においても言えることだと思います。

課題2「作業の並行性が低かった」について

この課題が発生した理由としては、課題1が発生した理由である「研究開発した技術の有効性を検証すること」を最優先にした結果であること、そして、スケジュールの影響により、木こりのジレンマが発生していたからだと考えています。

木こりのジレンマに関しては、一般的な商用開発でも度々話題に上がるかなと思っています。

 

課題に対して実施した施策

実施した施策としては以下の2つになります。

  1. ソフトウェアアーキテクチャの導入
  2. ブランチ戦略の策定

対策として要は、ソースコードをレイヤー別で扱えるようにした結果、テスタビリティと作業の並行性を上げる事に成功した、というのがこの施策の結果になります。

1. のソフトウェアアーキテクチャの導入では、結果的にはクリーンアーキテクチャを採用したのですが、こちらについては今回の課題を解決するために重要だった要素について、後編でお話したいと思います。

前編では、2. のブランチ戦略についてお話していきます。

我々System Integrationチームでは、合同会社DMM.comの認証認可チームさんが紹介されていた、short-lived feature branches版のトランクベース開発 を我々に合わせてカスタマイズして導入しました。

ブランチ

紹介されていた例では、mainブランチとfeatureブランチの2種類しか使用していなかったのですが、我々のチームでは、リリースブランチ的な役割としてmainブランチを使用し、もともとのmainブランチの役割をdevelopブランチに担わせています。

リリースブランチの資材をCIパイプラインでイメージビルドし、検証用のK8sにデプロイしています。

そして、マイルストーンとしてスケジュールに置かれているデモにおいては、この検証用の環境にデプロイした資材を使ってデモを行っています。

リリースブランチ的なブランチを追加した理由としては、チームで使用できる環境が先ほど紹介した1環境のみだったためです。環境が増やせなかった理由としては、依存関係のソフトウェア群がIaC化されておらず、手軽に環境を増やせなかったためです。(このあたりの改善についてはまた別の機会でお話したいと思います。)

つまり、開発環境であり検証環境(他のコンポーネントと結合した環境)でもあり、なおかつデモも控えていたりして、ある程度の安定性も求められるということから、このようなカスタマイズを施しました。

とはいえ意図していない変更がmainに混入してしまうというのは往々にして発生してしまうため、その場合はrouterやcontrollerの追加は最後に行うというようなルールを設定し、運用しています。

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

type Item struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

var items = []Item{
	{ID: 1, Name: "Item 1"},
	{ID: 2, Name: "Item 2"},
}

func main() {
	r := gin.Default()

	r.GET("/items", getItems)
	// r.POST("/items", createItem) 例えばrouterをコメントアウトしたり
	r.GET("/items/:id", getItemByID)

	r.Run(":8080")
}

// Controllerをコメントアウトする
// func getItems(c *gin.Context) {
// 	c.JSON(http.StatusOK, items)
// }

func createItem(c *gin.Context) {
	var newItem Item
	if err := c.ShouldBindJSON(&newItem); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	newItem.ID = len(items) + 1
	items = append(items, newItem)
	c.JSON(http.StatusCreated, newItem)
}

func getItemByID(c *gin.Context) {
	id := c.Param("id")
	for _, item := range items {
		if c.Param("id") == id {
			c.JSON(http.StatusOK, item)
			return
		}
	}
	c.JSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}

巷で紹介されているベストプラクティスはとても素晴らしいものです。しかしながら、それをそのまま現場のチームに導入するには、チームの台所事情や工数などとの兼ね合いなどを考えると難しい。だけども、現状を少しでも改善したいという想いのもと、今回の施策を実施しました。

同じような悩みを持った方の助けに少しでも慣れたら幸いです。

前編の終わりに

前編では、組織の課題を説明しました。プロダクション開発でもよくあるかもしれませんが、それがより深刻なのが研究開発の特徴かと思います。

次回の記事では、これらの課題に対して、ソフトウェアアーキテクチャ観点での施策において、私が重要だと考える要素について深掘りしていきますので、ぜひご期待ください。

後編は12月25日公開予定です。執筆頑張ります!

では、ソフトバンクアドベントカレンダーは21日目に続きます!

おすすめの記事

条件に該当するページがございません