1ヶ月間で「わける」技術を学んだ話
明治大学学生のインターン生が、タップルでの大規模 iOS アプリ開発において「PR を分ける」「責務を分ける」という分離の原則を実践し、開発効率と品質向上に寄与した事例報告である。
キーポイント
トランクベース開発における PR の細分化
1 回のレビュー時間を短縮し、マージ時の不具合リスクを減らすため、UI とロジックなどを細かく分けて主ブランチへ頻繁にマージする方針を採用。
責務の分離による疎結合なアーキテクチャ
Screen-Presenter-Service-Repository の階層構造を維持し、各層の責務を分散させることで大規模コードの管理性を高めている。
依存性注入を活用したテスト容易性の確保
@Dependency 属性を用いて Repository を注入することで、本番環境とテスト環境でのデータ切り替えを柔軟に行い、モックテストを可能にしている。
テスト環境での依存関係切り替え
@Dependency を使用することで、本番環境では実装(liveValue)を参照し、テスト環境ではモックデータ(testValue)を参照する切り替えを実現している。
疎結合なアーキテクチャの維持
Screen-Presenter-Service-Repository の階層構造で責務を分け、Dependency 経由でリポジトリにアクセスすることで、テスト容易性を保ちつつコード管理を容易にしている。
DependencyValues と DependencyKey の仕組み
DependencyValues を介して Repository を参照し、DependencyKey で liveValue と testValue を定義することで、環境に応じた振る舞いを統一的なインターフェースで制御している。
@MainActorと@concurrentの適切な使用
Service層などUI更新に関らない処理に@MainActorを付与すると並行実行が阻害されパフォーマンス低下の恐れがあるため、呼び出し元のactorを引き継がない@concurrentを活用して分離する必要がある。
重要な引用
PR を細かくわけることで、1 回のレビューにかかる時間を短縮でき、レビュワーの負担を減らすことができます
大規模開発においては、さまざまなものが肥大化しがちです。肥大化するとひとつひとつへの負荷が大きくなり...
テストではモックデータを使ってテストを行いたいので、本番環境とテスト環境で返す値を切り替える必要があります
テストではモックデータを使ってテストを行いたいので、本番環境とテスト環境で返す値を切り替える必要があります。
普通は liveValue の方を参照し、RepositoryImpl に定義された処理を実行します。テストの時には testValue を参照し、実際に API を叩かずにモックデータを参照します。
@MainActorを書きすぎると本来並行に実行できる処理までmain actor上で直列に実行され、パフォーマンスに悪影響を与える可能性があります
影響分析・編集コメントを表示
影響分析
この記事は、大規模 iOS アプリ開発における具体的なプラクティス(トランクベース開発、階層化アーキテクチャ、依存性注入)を学生レベルの実践から検証しており、エンジニアリング組織の文化やコード品質向上への示唆に富んでいます。特に、理論的な知識を実際の現場で適用する際の課題と解決策が具体的に記述されており、若手エンジニアや大規模開発に参画するチームにとって非常に参考になる内容です。
編集コメント
AI や最新技術のトレンド記事ではありませんが、大規模ソフトウェア開発における「分離と依存性管理」という普遍的なエンジニアリングの知見が、学生の実践を通じて鮮明に描かれており、実務家にとっても示唆に富む内容です。
はじめに
初めまして!明治大学商学部3年の伊藤汰海です。2026年3月の1ヶ月間、タップルのiOSエンジニアとしてインターンシップに参加させていただきました。
参加の経緯
今回私は、規模の大きいアプリ開発における技術を学びたいと思い、インターンに応募しました。これまでいくつかのインターンに参加させていただき、普段の個人開発でも大規模開発を意識するようになりました。しかしSwift ConcurrencyにおけるActorの管理に悩んだり、アーキテクチャにおいてどの層にどこまで責務を持たせるかなどについて悩むことが多くなっていました。このインターンを通じて、Swiftに対する自分の理解度を上げることを目標に、1ヶ月間学ばせていただきました。
わけることの大切さ
この1ヶ月間で、さまざまなものを「わける」ことの大切さを学びました。大規模開発においては、さまざまなものが肥大化しがちです。肥大化するとひとつひとつへの負荷が大きくなり、個人開発やハッカソンでは感じなかったデメリットが浮かび上がってきます。今回はこの負荷の下げ方についてたくさん学んだので、さまざまな「わける」について書こうと思います。
Pull requestをわける
まず1つ目が、Pull request(PR)をわけるということです。これまで私はコミットを細かくわけることを意識して開発してきました。しかしタップルでは、細かくPRを出してmainにマージする方針(トランクベース開発)を採用していました。PRを細かくわけることで、1回のレビューにかかる時間を短縮でき、レビュワーの負担を減らすことができます。また、派生ブランチをmainに都度マージしていくことで1回のマージによる差分が小さくなり、不具合が生じにくくなります。これにより毎週アプリをリリースすることが可能となり、スピード感のある開発が実現されていました。
しかし、PRをわけるといっても、すべてmainブランチから派生させるのは簡単ではありませんでした。コミットでわけていた頃は、それまで書いたコードが残った状態で次の段階の開発を進めることができました。しかしmainブランチから新たに派生させる場合、レビュー中の自分のコードが存在しない状態から別の開発を進めなければなりません。どういう単位や順番でPRを出すべきかを考えることは大変でしたが、その分、開発全体を見通す力が培われたと思います。今回は一度すべて実装し切ってから、UIとロジックでわけてPRを出し、その後に繋ぎ込むPRを出すという方針で進めました。しかし、レビュー修正後の動作確認の際に、ロジック側のコードも戻さないと変更後のUIが確認できないといった状況が生じ、修正コストが非常に高くなってしまいました。今後はPRをより細かい単位で出し、早くApproveをもらってmainにマージしながら作業を進めていきたいと思います。
責務をわける
大規模な開発では、ファイルやクラスなどを分割し、責務を分散させることで、それぞれを疎結合な状態に保つことにより、大量のコードを管理しやすくしています。この概念自体は知っており、前述したように個人開発においても意識しながら開発に取り組んでいました。タップルでは、おおまかに Screen-Presenter-Service-Repository という構成で開発を行っており、個人開発で学んでいた TCA(The Composable Architecture)に似た部分があり、私にとって理解しやすいものでした。しかし、このように分割していても各層はまだ大きいため、さらに細かく分割することもありました。特に Repository の Dependency(依存関係)の理解に時間がかかったため、共有したいと思います。
まず Repository は以下のように Service から呼び出して使用します。
@Dependency(\.repository) private var repository
func hoge() async -> Int {
return await repository.hoge()
}
なぜ普通にインスタンス化して使わないのかというと、テストのためです。テストではモックデータを使って行いたいので、本番環境とテスト環境で返す値を切り替える必要があります。そこで Dependency を使って実装しています。@Dependency を使うと、以下の場所にアクセスします。
extension DependencyValues {
public var repository: Repository {
get { self[Repository.self] }
set { self[Repository.self] = newValue }
}
}
DependencyValues を通して Repository を参照しています。これを通すことで以下のようなことができます。
extension Repository: DependencyKey {
public static var liveValue: Self {
let impl = RepositoryImpl()
return .init(
hoge: { try await impl.hoge() }
)
}
public static let testValue = mock
}
普通は liveValue を参照し、RepositoryImpl に定義された処理を実行します。テストの時には testValue を参照し、実際に API を叩かずにモックデータを参照します。
@DependencyClient
public struct Repository: Sendable {
public var hoge: @Sendable () async throws -> String
}
private struct RepositoryImpl: Sendable {
func hoge() async throws -> String {
return try await fetchData()
}
}
このように関数自体の定義と、中身の処理も分かれています。
Dependency を使いたいという理由もあるとは思いますが、それでも Repository でこれだけ細かく分割されているのは、とても面白かったです。
MainActorから処理を分ける
Swift 6からはデータの競合安全性のチェックが強化され、コンパイル時に問題が検出されるようになったため、データの競合に関して個人開発でも意識するようになりました。しかし理解が乏しいまま実装を行っていて、とりあえず@Sendableや@MainActorをつけておけば直るという認識(それぞれの意味についてはなんとなく知っている程度)でした。しかし今回のインターン中に、Service層のロジックに@MainActorを付与する必要はない、というレビューをいただきました。
@MainActorを付与したコードはmain actorに隔離され、その上で実行されます。UIの更新はmain actorで行う必要がある、というところまでは理解していました。しかし今回Service層(直接ビューの更新は行わない場所)に@MainActorを付与したことに指摘をいただきました。書いた時の私は、並行処理に関するエラーが出た際に、直るからという理由だけで@MainActorをつけて解消していました。しかし@MainActorを書きすぎると本来並行に実行できる処理までmain actor上で直列に実行され、パフォーマンスに悪影響を与える可能性があります。そのため、UIの更新に直接関係ない処理は必要以上にmain actorに載せず、適切に分離することが重要です。
これまでは、main actor上から呼び出しても、その処理がmain actorを引き継がずに実行されるケースがありました。しかしSwift 6.2からは、呼び出し元のactorを引き継いで、そのactorに隔離された状態で実行されるようになりました。これによって、main actorがServiceまで伝播してしまっていたのです。そこで使ったのが@concurrentです。@concurrentを付与することで、その関数は呼び出し元のactor を引き継がず、並行に実行されるようになります。今回はこの理解を深め、main actorからServiceの処理を分離できました。
フォルダを分ける
ディレクトリ構成の話ではなく、gitでブランチごとにフォルダを分けられるというものです。git worktreeというものがあり、ブランチごとにフォルダを生成することができます。これによって、複数のブランチをひとつのウィンドウで管理する必要はなく、別々のブランチをそれぞれ開くことで、ウィンドウを移動するだけでブランチを移動することができるのです。作業中にレビューが返ってきたりしてすぐに修正したいときに、いちいちgit stashすることなく作業を継続できて、とてもやりやすかったです。ただ、コマンドが長かったりするのでwtpというツールを使っていました。今回は時間がなくてできなかったのですが、wtpではなく自前でカスタムコマンドを組んで使っている人もいたので、個人開発などでカスタムコマンドなどを組んで使ってみたいです。
その他インターンしていてすごいと感じたこと
E2Eテストの自動化
従来、テスターが手動で実機を操作してテストを行っていた項目の一部を、Maestro を用いて自動化する取り組みです。テストには、手順が決まっている「手順テスト」と、自由に操作する「フリーテスト」の 2 種類があります。これまでバグが発見されるケースはフリーテストで多く見られたため、そちらにより多くの時間を割けるようにしたいという背景があります。また、自動化によって手動作業を削減し、テストの実施頻度を上げることで、バグ発見までのスピードを向上させることも目的のひとつです。自動化の仕組みとしては、YAML ファイルに操作手順や確認項目を記述し、それらが満たされているかをチェックする形で行います。今回は 2 つのテストを作成しましたが、非常に簡潔に記述できました。他にも SwiftUI 化が進んでいたりと、技術的なアップデートがかなり進んでいると感じました。ユーザーにとって変化がないものは他の施策よりも後回しにされがちですが、技術面もしっかりアップデートしていてすごいなと思いました。
AI 活用
AI への感度がとても高い環境だと感じました。エンジニアが AI を活用するのはもちろん、ビジネス職のメンバーも積極的に AI を業務効率化に役立てている雰囲気が伝わってきました。特に印象的だったのは、エンジニアの中にもほとんど自分ではコードを書かず、AI に任せて開発を進めている方がいたことです。また、チーム内では新たに発表された機能の共有なども行われており、キャッチアップの速さにも驚かされました。さらに、インターン生にもアカウントを付与していただいたおかげで、既存実装の確認にかかる時間を大幅に短縮でき、その分コードの理解や重要な部分の実装に集中できたため、非常に密度の濃い 1 ヶ月を過ごすことができました。
まとめ
今回のインターンでは技術はもちろん、今後の就活やキャリアについても考えることができました。毎回ランチは社員の方と行くことができ、色んな方のお話を聞くことができました。また毎週人事の方と話す機会もあり、今回のインターンの目標や、今後就活する上で重視する軸などたくさんのことを相談させていただきました。
最後に、今回のインターンでお世話になったトレーナーさん、担当人事の方、受け入れ部署の方、ランチや面談をしてくださった皆さん、本当にありがとうございました!
原文を表示
はじめに
はじめまして!明治大学商学部3年の伊藤汰海です。2026年3月の1ヶ月間、タップルのiOSエンジニアとしてインターンシップに参加させていただきました。
参加の経緯
今回私は、規模の大きいアプリ開発における技術を学びたいと思い、インターンに応募しました。これまでいくつかのインターンに参加させていただき、普段の個人開発でも大規模開発を意識するようになりました。しかしSwift ConcurrencyにおけるActorの管理に悩んだり、アーキテクチャにおいてどの層にどこまで責務を持たせるかなどについて悩むことが多くなっていました。このインターンを通じて、Swiftに対する自分の理解度を上げることを目標に、1ヶ月間学ばせていただきました。
わけることの大切さ
この1ヶ月間で、さまざまなものを「わける」ことの大切さを学びました。大規模開発においては、さまざまなものが肥大化しがちです。肥大化するとひとつひとつへの負荷が大きくなり、個人開発やハッカソンでは感じなかったデメリットが浮かび上がってきます。今回はこの負荷の下げ方についてたくさん学んだので、さまざまな「わける」について書こうと思います。
Pull requestをわける
まず1つ目が、Pull request(PR)をわけるということです。これまで私はコミットを細かくわけることを意識して開発してきました。しかしタップルでは、細かくPRを出してmainにマージする方針(トランクベース開発)を採用していました。PRを細かくわけることで、1回のレビューにかかる時間を短縮でき、レビュワーの負担を減らすことができます。また、派生ブランチをmainに都度マージしていくことで1回のマージによる差分が小さくなり、不具合が生じにくくなります。これにより毎週アプリをリリースすることが可能となり、スピード感のある開発が実現されていました。
しかし、PRをわけるといっても、すべてmainブランチから派生させるのは簡単ではありませんでした。コミットでわけていた頃は、それまで書いたコードが残った状態で次の段階の開発を進めることができました。しかしmainブランチから新たに派生させる場合、レビュー中の自分のコードが存在しない状態から別の開発を進めなければなりません。どういう単位や順番でPRを出すべきかを考えることは大変でしたが、その分、開発全体を見通す力が培われたと思います。今回は一度すべて実装し切ってから、UIとロジックでわけてPRを出し、その後に繋ぎ込むPRを出すという方針で進めました。しかし、レビュー修正後の動作確認の際に、ロジック側のコードも戻さないと変更後のUIが確認できないといった状況が生じ、修正コストが非常に高くなってしまいました。今後はPRをより細かい単位で出し、早くApproveをもらってmainにマージしながら作業を進めていきたいと思います。
責務をわける
大規模開発では、ファイルやクラスなどをわけ、責務を分散しそれぞれを疎結合な状態に保つことで大量のコードを管理しやすいようにしています。それ自体は知っていたし、前述したように個人開発でも意識しながら開発に取り組んでいました。タップルでは大まかに、Screen-Presenter-Service-Repositoryというような構成で開発を行っていて、個人開発で勉強していたTCAに似たようなところがあり、自分にとって理解しやすいものではありました。しかし、このようにわけていてもそれぞれの層はまだ大きいので、さらに細かくわけていたりもしていました。特にRepositoryのDependency関係は理解するのに時間がかかったので共有したいと思います。
まずRepositoryは以下のようにServiceで呼び出して使用します。
@Dependency(\.repository) private var repository
func hoge() async -> Int {
return await repository.hoge()
}なぜ普通にインスタンス化したりして使わないのかというと、テストのためです。テストではモックデータを使ってテストを行いたいので、本番環境とテスト環境で返す値を切り替える必要があります。そこでDependencyを使って実装しています。@Dependencyを使うと、以下の場所にアクセスします。
extension DependencyValues {
public var repository: Repository {
get { self[Repository.self] }
set { self[Repository.self] = newValue }
}
}DependencyValuesを通してRepositoryを参照しています。これを通すことで以下のようなことができます。
extension Repository: DependencyKey {
public static var liveValue: Self {
let impl = RepositoryImpl()
return .init(
hoge: { try await impl.hoge() }
)
}
public static let testValue = mock
}普通はliveValueの方を参照し、RepositoryImplに定義された処理を実行します。テストの時にはtestValueを参照し、実際にAPIを叩かずにモックデータを参照します。
@DependencyClient
public struct Repository: Sendable {
public var hoge: @Sendable () async throws -> String
}
private struct RepositoryImpl: Sendable {
func hoge() async throws -> String {
return try await fetchData()
}
}このように関数自体の定義と、中身の処理もわかれています。
Dependencyを使いたいというのもあるとは思いますが、それでもRepositoryでこれだけ細かくわかれているのは、とても面白かったです。
MainActorから処理をわける
Swift 6からはデータの競合安全性のチェックが強化されコンパイル時に問題が検出されるようになったため、データの競合に関して個人開発でも意識するようになりました。しかし理解が乏しいまま実装を行っていて、とりあえず@Sendableや@MainActorをつけておけば直るという認識(それぞれの意味についてはなんとなく知っている程度)でした。しかし今回のインターン中に、Service層のロジックに@MainActorを付与する必要はない、というレビューをいただきました。
@MainActorを付与したコードはmain actorに隔離され、その上で実行されます。UIの更新はmain actorで行う必要がある、というところまでは理解していました。しかし今回Service層(直接ビューの更新は行わない場所)に@MainActorを付与したことに指摘をいただきました。書いた時の私は、並行処理に関するエラーが出た際に、直るからという理由だけで@MainActorをつけて解消していました。しかし@MainActorを書きすぎると本来並行に実行できる処理までmain actor上で直列に実行され、パフォーマンスに悪影響を与える可能性があります。そのため、UIの更新に直接関係ない処理は必要以上にmain actorに載せず、適切に分離することが重要です。
これまでは、main actor上から呼び出しても、その処理がmain actorを引き継がずに実行されるケースがありました。しかしSwift 6.2からは、呼び出し元のactorを引き継いで、そのactorに隔離された状態で実行されるようになりました。これによって、main actorがServiceまで伝播してしまっていたのです。そこで使ったのが@concurrentです。@concurrentを付与することで、その関数は呼び出し元のactor を引き継がず、並行に実行されるようになります。今回はこの理解を深め、main actorからServiceの処理を分離できました。
フォルダをわける
ディレクトリ構成の話ではなく、gitでブランチごとにフォルダをわけられるというものです。git worktreeというものがあり、ブランチごとにフォルダを生成することができます。これによって、複数のブランチをひとつのウィンドウで管理する必要はなく、別々のブランチをそれぞれ開くことで、ウィンドウを移動するだけでブランチを移動することができるのです。作業中にレビューが返ってきたりしてすぐに修正したいときに、いちいちgit stashすることなく作業を継続できて、とてもやりやすかったです。ただ、コマンドが長かったりするのでwtpというツールを使っていました。今回は時間がなくてできなかったのですが、wtpではなく自前でカスタムコマンドを組んで使っている人もいたので、個人開発などでカスタムコマンドなどを組んで使ってみたいです。
その他インターンしていてすごいと感じたこと
E2Eテストの自動化
従来、テスターが手動で実機を操作してテストを行っていた項目を、Maestroを使って一部自動化する取り組みです。テストには、手順が決まっている手順テストと、自由に操作するフリーテストの2種類があります。これまでバグが発見されるケースはフリーテストで多く見られたため、そちらにより多くの時間を割けるようにしたいという背景があります。また、自動化によって手動作業を削減し、テストの実施頻度を上げることで、バグ発見までのスピードを向上させることも目的のひとつです。自動化の仕組みとしては、YAMLファイルに操作手順や確認項目を記述し、それらが満たされているかをチェックする形で行います。今回は2つのテストを作成しましたが、非常に簡潔に記述できました。他にもSwiftUI化が進んでいたりと、技術的なアップデートがかなり進んでいると感じました。ユーザーにとって変化がないものは他の施策よりも後回しにされがちですが、技術面もしっかりアップデートしていてすごいなと思いました。
AI活用
AIへの感度がとても高い環境だと感じました。エンジニアがAIを活用するのはもちろん、ビジネス職のメンバーも積極的にAIを業務効率化に役立てている雰囲気が伝わってきました。特に印象的だったのは、エンジニアの中にもほとんど自分ではコードを書かず、AIに任せて開発を進めている方がいたことです。また、チーム内では新たに発表された機能の共有なども行われており、キャッチアップの速さにも驚かされました。さらに、インターン生にもアカウントを付与していただいたおかげで、既存実装の確認にかかる時間を大幅に短縮でき、その分コードの理解や重要な部分の実装に集中できたため、非常に密度の濃い1ヶ月を過ごすことができました。
まとめ
今回のインターンでは技術はもちろん、今後の就活やキャリアについても考えることができました。毎回ランチは社員の方と行くことができ、色んな方のお話を聞くことができました。また毎週人事の方と話す機会もあり、今回のインターンの目標や、今後就活する上で重視する軸などたくさんのことを相談させていただきました。
最後に、今回のインターンでお世話になったトレーナーさん、担当人事の方、受け入れ部署の方、ランチや面談をしてくださった皆さん、本当にありがとうございました!
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み