こんにちは!しおまち / shiomachiです。普段はパーサージェネレータを自作するなど、主に言語処理系の開発に取り組んでいます。
夏休みに、合同会社DMM.comが主催したワークショップ型インターン「Go Sprint」に参加したので、参加記録を兼ねてGo言語におけるアーキテクチャについて記事を書きたいと思います。
期間:2026年8月3日から8月7日
プログラミング / Golang経験
・プログラミングはいろいろやってきてはいましたが、Go言語の経験はだいたい半年
インターンを志望/参加した理由
DMMでは、動画配信サービスなど、多岐にわたるコンテンツ配信事業を展開しています。
私自身、個人開発で映像コミュニティを支える基盤を開発していることもあり、「コンテンツの作り手の思いや感情を世界に届けることを支えたい」という目標があります。そのため、動画配信やコンテンツ配信を支えるバックエンドの技術に興味があります。
特にtoC向けのコンテンツ配信サービスでは、大量の動画や画像を扱うことに加え、多くのユーザーからの同時アクセスやアクセス数の変動にも対応する必要があり、スケーラビリティや安定性が求められます。その一方で、機能追加や事業の成長に伴うシステムの変更にも対応する必要があり、こうした将来を見据えた開発や、それを支える技術的な意思決定に面白さを感じています。(私はよく「生みの苦しみ」と表現しており、生みの苦しみを楽しむことをモットーにしています)
今回のGo Sprintでは、こうしたバックエンド開発における設計の考え方を実装を通じて学べると考え、応募しました。
やったこと / オニオンアーキテクチャの概要
今回のGo Sprintでは、オニオンアーキテクチャを用いたAPI開発に取り組みました。
システム開発において、データベースのテーブル構造を変更することがあります。DBスキーマを変更すれば、アプリケーション側のコードも場合によっては修正する必要があります。また、DBスキーマの変更はチーム内で適切に共有・調整する必要があります。そのため、先にビジネスロジックを実装しておき、後からデータベースへのアクセス処理を実装したいケースもあるでしょう。
さらに、DBスキーマの変更だけでなく、ライブラリやフレームワーク、データベースそのものの変更によって、既存のコードを修正しなければならない場合もあります。
例えば、MySQL 5.7では次のようにGROUP BY句に直接ソート順を指定できました。
SELECT category, COUNT(*)
FROM products
GROUP BY category DESC;
しかし、MySQL 8.0.13以降ではこの構文がサポートされなくなり、次のようにORDER BY句を用いて記述する必要があります。
SELECT category, COUNT(*)
FROM products
GROUP BY category
ORDER BY category DESC;
▲ 参考:MySQL 8.0 リファレンスマニュアル — SELECTステートメント
ビジネスロジックの中にSQLなどのデータベースアクセス処理が直接記述されていると、アプリケーションが実現したい機能が変わらなくても、データベース側の変更によってビジネスロジックを記述したコードも修正する必要が生じます。
そこで、ビジネスロジックとデータベースへのアクセス処理を分離するという考え方があります。これにより、SQLの変更などをデータベースアクセス処理の実装側で吸収し、ビジネスロジックへの影響を抑えることができます。
このような関心の分離を、依存関係の方向も含めて実現するアーキテクチャの一つが「オニオンアーキテクチャ」です。
インターンの進めかた
インターンは5日間の日程で構成されており、最初の3日間はオンラインで、4日目と5日目は実際に出社し対面で実装を進めていきました。初日にはGo言語の概要をひととおり学んだあと、2日目からは、与えられたAPIエンドポイントの実装課題に取り組みました。
対面では特にチームとのコミュニケーションに注力しており、同じチームの方とわからないことや知見をSlackや対面で共有することを意識していました。
実はDMM本社オフィスには2022年に未踏ジュニアで採択された際に訪問させていただいたことがあります。4年ぶりの訪問だったので、少し感動しました(?)
依存性逆転の基礎
オニオンアーキテクチャの基礎となる概念として、「依存性逆転の原則」があります。
よくあるシンプルな実装として、ビジネスロジックがデータベースアクセスの具体的な実装を直接利用する場合、データベースアクセス側の実装を変えたり、差し替えたりしようとすると、ビジネスロジック側の処理まで修正が必要になることがあります。
ビジネスロジックが具体的な実装の詳細を知らずに処理を実行するにはどのようにすれば良いでしょうか。また後述するテストなどのことを考えれば、ビジネスロジック側を変更せずに具体的な実装を差し替えることができれば一石二鳥です。
▲ インターフェースを利用した実装
そこで、必要な操作をインターフェースとして定義してみましょう。
このようにすれば、ビジネスロジックはそのインターフェースを利用すればよく、下位のデータアクセスの詳細を知らずに組み立てることができます。
これが依存性逆転の原則の考え方です。
オニオンアーキテクチャでは、この原則を利用して、中心となるビジネスロジックを外部技術の具体的な実装から切り離しています。
オニオンアーキテクチャの基礎
▲ オニオンアーキテクチャの概念図
オニオンアーキテクチャについて「ドメインモデルを中心に据え、ソースコードの依存関係が外側の層から内側の層へ向かうように設計するアーキテクチャ」という説明がなされていると思います。実際それぞれの層はどんな役割を持っているのでしょうか。ざっくりと各層の役割を解説していきます。
ドメインモデル
まず一番の中心となるのはドメインモデルです。
ユーザーなどのドメイン上の概念を、状態(データ)と振る舞い(処理)によって表現します。
状態(データ)の例
- ユーザーのIDやユーザー名
- 商品の名前や価格
- SNSの投稿内容
振る舞い(処理)の例
- 商品の価格を変更するとき、0円未満の価格を設定できないようにする
- SNSの投稿内容を変更するとき、文字数の上限を超えないようにする
オニオンアーキテクチャでもっとも大事になるのは、これらの状態や振る舞いが、データアクセスなどの外の層の詳細を知らないようにするということです。
ドメインサービス
特定のドメインモデルだけでは扱いにくいビジネス上のルールや処理をまとめたものです。
例
- ユーザー名の重複確認(ほかのユーザーと同じユーザー名で登録できないようにする)
- 口座間の送金(送金元の残高と送金先の口座を考慮し、送金が可能か判断する)
- 商品の割引額の計算(複数の商品や顧客の会員ランクなどを考慮して割引額を計算する)
アプリケーションサービス
ドメインモデルやドメインサービスを組み合わせて、アプリケーションの機能(ユースケース)を実現するものです。
- ユーザー登録(ユーザー名の重複確認 → ユーザー生成 → 保存)
- 商品購入(在庫確認 → 注文作成 → 在庫更新 / 決済処理)
インフラ
データベースへのアクセスや外部APIとの通信など、外部技術に関する具体的な処理を実装する層です。技術的な変更の影響を受けやすい部分です。
例
- ユーザー情報の保存(SQLのINSERT文を実行)
- ユーザー情報の取得(SQLのSELECT文を実行)
- メール送信(外部のメール配信APIを呼び出す)
- 外部サービスとの通信(HTTP APIを呼び出してデータを取得)
テストも楽になるかも!
Webアプリケーションのテストを書くとき、データベースが絡むテストを書くのは(一般的には)めんどくさいですよね。データベースを起動して、データを投入して・・・という操作を行わなければなりませんし、テストどうしが干渉しないようにする必要もあります。
こんな時にもオニオンアーキテクチャや依存性逆転の原則を用いることができます。例えば、データベースへのアクセス処理をインターフェースとして抽象化しておけば、テスト時にはそのインターフェースを満たすテスト用の実装に差し替えることができます。依存性逆転の原則を利用することで、データベースに実際に接続することなく、ユースケースのテストを行いやすくなるわけです。
また、Goではinterfaceからテスト用のMock実装を自動生成できるGoMockというツールも存在しており、これを用いることで、テストごとにメソッドの戻り値やエラーを指定できるほか、期待するメソッドが呼び出されたかどうかも検証できます。
このように、依存先をinterfaceとして抽象化しておくことで、Mockの自動生成を行うこともでき、テストも行いやすくなります。
▲参考:GoMock
さらに、外部I/Oへの依存をテスト用の実装に置き換えた単体テストは、CIパイプラインでも実行しやすいという利点があります。
name: Go CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run tests
run: go test ./... -v
▲ インターン期間中には個人的にCIパイプラインの整備をしたりしてみました。
個人開発のファイルサーバーにオニオンアーキテクチャを採用してみるときに考えること
▲ 映像コミュニティ向けに開発しているファイルサーバーのバックエンド実装
現在、私が個人開発・運用しているファイルサーバー「みつばち」はRuby on Railsで構築していますが、仮にGoで書き直す場合には、オニオンアーキテクチャも選択肢の一つとして考えています。
ゴミ箱への移動と復元処理
例えば、みつばちのゴミ箱関係の処理には次の機能があります
- 複数のファイルやフォルダを一括してゴミ箱に移動する(ストレージには存在したまま)
- 反対にゴミ箱に移動したファイルやフォルダを復元する
- ゴミ箱にあるファイルを物理的にストレージからも削除する
これらの機能を実現するには次のような処理が必要です。
- 対象となるファイルやフォルダの現在の状態を取得する
- フォルダの場合、配下のファイルを再帰的にチェックする
- ユーザーの操作権限やファイルの状態を確認し、削除・復元が可能か判断する
- 対象となるファイルやフォルダの削除・復元処理を実行する
これらの機能をどの粒度でそれぞれの層に分解すれば、読みやすくメンテナンスをしやすいコードになるでしょうか?
ドメインモデル
まずドメインモデルとしてあげられるのは「ファイル」や「フォルダ」構造でしょう。ファイル名や削除されているかのフラグ、どのユーザーが投稿したファイルなどかを記録するものです。それに付随してファイル名の命名の制約(空白はNGなど)もモデルの一部として扱うことができそうです。
ドメインサービス
ファイルやフォルダに対する操作権限の判定がドメインサービスにあたると考えられます。
ファイルの所有者だけでなく、所属する組織や共有設定などは複数の情報を考慮して操作の可否を判断する必要があるためです。
アプリケーションサービス
これまで記載したドメインモデルやドメインサービスを組み合わせることによって、ユーザが操作する機能を実現できそうです。
例えば、複数のファイルをゴミ箱へ移動する場合、対象となるファイルを取得し、操作権限やファイルの状態を確認したうえで、削除状態を変更して保存する、といった流れになります。
インフラ
データベースからファイルを取得したり、変更した内容を保存したりする処理はインフラ層に分離します。こうすることで、ドメインモデルなどのコアとなる部分は、具体的なSQL文を知らずに処理できるようになります。
悩ましいところ
このシステムにオニオンアーキテクチャを導入するうえで、特に難しく工夫が必要になると考えているのは、ストレージからファイル本体を削除する物理削除機能です。削除に関する処理をドメイン層・アプリケーション層・インフラ層にどのように分離するか、また、データベースのトランザクションの範囲をどのように設定し、ストレージ上のファイルとの整合性を保つかが課題になると考えています。
例えば、ファイルを完全に削除する場合、データベース上のファイル情報の更新に加えて、ストレージ上のファイル本体を削除する必要があります。しかし、データベース上の更新に成功しても、ストレージ上のファイル削除に失敗する可能性があります。また、その逆も考えられます。しかもこの処理が不完全な状態で終わってしまうと、ファイルはもう存在しないのに、ユーザにはファイルがあるように見えることが考えられるため、慎重に実装しなければなりません!
解決策としては、ストレージからファイルそのものを削除する処理は、トランザクションの後に切り離してしまうという選択肢が考えられるでしょう。そうすればトランザクションが失敗すればファイルは消去されませんし、仮にトランザクション内の処理が成功したもののファイルを削除できなかった場合でも、利用者への影響を抑えることができます。
余談
▲ ちなみに、個人開発で長期的に保守しているGoプロジェクトでは、クリーンアーキテクチャを採用しています。勉強会で利用するサービスとして開発を続けているため、機能追加や変更のしやすさを意識して設計しています。
DMMならではの研修型インターンのおもしろさ
インターン中には、雑談の延長として社員さんに個人開発で実装に悩んでいた機能について相談したり、ビアバッシュ(LT会)で気になったことを質問したりする機会もありました。(質問する側なのに緊張しちゃったけど・・・)
特に印象に残っているのは、インターンの課題とは直接関係のない個人開発の悩みにも、社員さんが真摯に向き合ってくださったことです。技術的な相談に対して一緒に考えていただけた経験から、実際の開発現場でも、仕様や実装方針について立場や担当領域を超えて議論しながら、よりよいものを作っていけそうな社風を感じました。
インターンの課題を通じた技術的な学びはもちろん、普段の個人開発についても社員さんと議論できたことは、今回のGo Sprintに参加してよかったと感じる大きな理由の一つです。
まとめ
今回のGo Sprintでは、オニオンアーキテクチャを用いたAPI開発を通じて、将来的な変更を見据えた設計の考え方を学ぶことができました。
また、チームメンバーとの技術的な議論や、社員さんとの交流を通じて、自分一人では気づかなかった視点に触れることもできました。
特に、個人開発で悩んでいたことについても社員さんと議論できたことは、大きな収穫でした。技術的な課題に対して、さまざまな立場や視点から考え、よりよい実現方法を模索していくことのおもしろさを改めて感じた5日間でした。
今回学んだことを、今後の個人開発にも活かしていきたいと思います!







