はじめに

はじめまして!明治大学商学部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ヶ月を過ごすことができました。

 

まとめ

今回のインターンでは技術はもちろん、今後の就活やキャリアについても考えることができました。毎回ランチは社員の方と行くことができ、色んな方のお話を聞くことができました。また毎週人事の方と話す機会もあり、今回のインターンの目標や、今後就活する上で重視する軸などたくさんのことを相談させていただきました。

 

最後に、今回のインターンでお世話になったトレーナーさん、担当人事の方、受け入れ部署の方、ランチや面談をしてくださった皆さん、本当にありがとうございました!