Booost!!! Excite Internship 2025 でのエンジニア業務体験記

はじめに

初めまして!2025年10月の1ヶ月間 "Booost!!! Excite Internship 2025" に参加させていただいた、加藤といいます!

この記事では、このインターンシップで私が取り組んだ業務内容や、そこから得た学び等について紹介していければと思います。

自己紹介

私は情報工学専攻の修士1年生です。研究室の方では、AIの応用領域の研究を行っています。

私が "Booost!!! Excite Internship 2025" に参加しようと思った理由は、リアルなITエンジニアの業務を体験して、エンジニアとして働くことの解像度を高めたかったためです。

インターンシップでの業務内容

概要

「お悩み相談室」というサービスの中の「コラム詳細ページ」の、旧実装から新実装への移植作業を行いました。旧実装と新実装では、データ取得方法やアーキテクチャの構造などが異なるため、旧実装の実装内容を理解した上で、新実装のアーキテクチャの形式に変換していくことが求められる業務でした。

以降の説明では、新実装のアプリケーションを「本実装」として、新実装の説明を行っていきます。

本実装のアーキテクチャ構造

レイヤードアーキテクチャ

本実装では、レイヤードアーキテクチャ(層構造の設計)を採用しています。具体的には、以下のように役割ごとに層を分けて、それぞれが連携して動作するアプリケーションとなっています。

【リクエストの流れ】
ユーザのブラウザ
    ↓
① Controller層(入力の受付、案内係)
    ↓
② UseCase層(ビジネスロジック係)
    ↓
③ Repository層(データ取得係)
    ↓
④ ViewModel層(表示用データ整形係)
    ↓
⑤ View層(HTML表示係)
    ↓
ユーザのブラウザに表示
Controller層

リクエストを受け付け、UseCase と ViewModel を呼び出し、最終的に View に表示を指示する、司令塔の役割を果たします。ここにはビジネスロジック(複雑な計算や判断)は書かず、処理の振り分けに専念します。具体的には以下のような処理を行うことが考えられます。

<?php
/**
 * ユーザ一覧ページを表示する
 */
public function index(Request $request): View
{
     // 1. UseCaseにデータの取得を依頼
     // (例: 'active' ステータスのユーザのみ取得するよう依頼)
     $userDomainData = $this->fetchUserListUseCase->execute('active');

     // 2. 得られたデータをViewModelに渡してデータの整形を依頼
     // (ドメインデータを、Viewが表示しやすい形に加工)
     $viewModel = $this->userListViewModel->build($userDomainData);

     // 3. 得られたデータをViewに渡してデータの表示を依頼
     // (Laravelの view() ヘルパー関数)
     return view('users.index', [
         'viewModel' => $viewModel // 整形済みデータをViewに渡す
     ]);
}
UseCase層

アプリケーション固有のビジネスロジックを実行します。具体的には以下のように、Repository からデータを取得し、必要な処理を加えます。

<?php
/**
 * ビジネスロジックを実行する
 */
public function execute(string $status): array
{
    // 1. Repositoryにデータの取得を依頼
    $allUsers = $this->userRepository->findByStatus($status);

    // 2. 得られたデータにロジックを適用
    // (例: ビジネスルールに基づき、スコアが100以上のユーザのみに絞り込む)
    $highScorers = [];
    foreach ($allUsers as $user) {
        if ($user->score >= 100) {
            // (例: ビジネスルールに基づき、名前を大文字に変換する)
            $user->name = strtoupper($user->name);
            $highScorers[] = $user;
        }
    }

    // 3. データを返却する (ドメインデータ)
    return $highScorers;
}
Repository層

データベースやJSONファイルなど、具体的なデータ保存場所とのやり取りを記述します。「どこから」「どうやって」データを取るかはこの層だけが知っている、すなわち、データ取得方法が隠蔽(抽象化)されているため、Repository層のデータ取得方法が変わったとしても、呼び出し側(UseCase)は影響を受けません。具体例は以下のようになります。

<?php
/**
 * データを取得する
 */
public function findByStatus(string $status): array
{
    // 1. JsonファイルやDBなどからデータを取得
    
    // (例: Eloquent ORM を使う場合)
    $users = User::where('status', $status)
                 ->where('deleted_at', null) // 削除済みは除外
                 ->get();

    /*
    // (例: クエリビルダ を使う場合)
    $users = DB::table('users')
               ->where('status', $status)
               ->get();
    */
    
    // 2. データを返却する (例: 配列に変換)
    return $users->toArray();
}
ViewModel層

UseCase から受け取ったドメインデータ(ロジック適用済みのデータ)を、View がそのまま表示できる形式に加工します。ViewModelで事前に整形を行うことによって、Viewファイル内で複雑な処理を書かないようにしています。具体例は以下のようになります。

<?php
/**
 * データを表示用に加工する
 */
public function build(array $userDomainData): self
{
    $this->userCount = count($userDomainData);
    $this->pageTitle = "ユーザ一覧 (" . $this->userCount . "名)";
    
    $processedUsers = [];
    
    // 1. Viewで使いやすいようにデータを表示用に加工
    foreach ($userDomainData as $user) {
        
        // (例: 登録日を 'Y年m月d日' 形式にフォーマット)
        $joinedDate = Carbon::parse($user['created_at'])->format('Y年m月d日');
        
        // (例: Viewでの強調表示フラグを追加)
        $isPremium = $user['plan_type'] === 'premium';

        $processedUsers[] = [
            'displayName' => $user['name'] . '様', // '様' を付ける
            'joinedFormatted' => $joinedDate,
            'isPremiumUser' => $isPremium,
        ];
    }

    $this->displayUsers = $processedUsers;

    // 2. データを返却する (自分自身のインスタンス)
    return $this;
}
View層

Controller から渡された ViewModel のデータを使って、HTMLを組み立てます。複雑なロジックは書かず、「表示するだけ」に徹します。具体例は以下のようになります。

{{--
  $viewModel (UserListViewModelのインスタンス) を
  Controllerから受け取る
--}}

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ $viewModel->pageTitle }}</title>
    <style>
        /* ViewModelのフラグに応じてスタイルを適用 */
        .premium {
            font-weight: bold;
            color: gold;
        }
    </style>
</head>
<body>
    {{-- 1. 受け取ったデータをHTMLとして表示 --}}
    
    <h1>{{ $viewModel->pageTitle }}</h1>
    <p>合計 {{ $viewModel->userCount }} 名のユーザーが見つかりました。</p>

    <ul>
        {{-- $viewModel内の加工済みリストをループ処理 --}}
        @foreach ($viewModel->displayUsers as $user)

            {{-- ViewModelのフラグを使ってCSSクラスを動的に変更 --}}
            <li class="{{ $user['isPremiumUser'] ? 'premium' : '' }}">
            
                {{-- ViewModelで加工済みのデータを表示するだけ --}}
                <strong>{{ $user['displayName'] }}</strong>
                (登録日: {{ $user['joinedFormatted'] }})
            </li>

        @endforeach
    </ul>

</body>
</html>
なぜこんなに複雑な構造にするのか?(アーキテクチャの設計思想)

各層が1つの役割だけを持って独立することで、どこを修正すればいいかが明確になり、個別にテストが行いやすくなり、将来の変更に強くなります。これを「責任の分離」といいます。

例えば、

  • 表示が崩れた → View層だけ修正
  • データ取得方法を変更 → Repository層だけ修正
  • ビジネスルール変更 → UseCase層だけ修正
  • Laravelから別のフレームワークに移行 → Controller, ViewModel, View だけ変更

のように修正すべきファイルを明確にし、将来の変更に強くなります。

アーキテクチャの設計思想についてより詳しく知りたいと思った方は、以下のWebページ等を参考にしてみてください。

クリーンアーキテクチャ完全に理解した · GitHub

Model View ViewModel - Wikipedia

UseCase層、Repository層に見られる3ファイル構成について

この設計では、UseCase層、Repository層が3種類のファイルで構成されています。これは契約(Interface)、データ転送(Output)、実装(Implementation)を明確に分離する設計パターンです。

Interface

この機能(UseCase、Repository等)が「何を受け取り」「何を返すか」だけを定義します。つまり、チーム内の「合意」であり、使う側はこの契約書だけを見て開発できるようになります。具体例は以下のようになります。

<?php
/**
 * 契約書 (Interface)
 *
 * 役割: 「ユーザー一覧を取得する」機能の仕様を定義する。
 * - 入力: (string) $status ... 'active' や 'pending' などの状態
 * - 出力: (FetchUserListOutput) $output ... 専用のデータ構造
 *
 * 呼び出し側(Controller)は、このInterfaceにのみ依存する。
 */
interface FetchUserListUseCaseInterface
{
    /**
     * ユースケースを実行する
     *
     * @param string $status 取得したいユーザーのステータス
     * @return FetchUserListOutput 処理結果のデータ構造
     */
    public function handle(string $status): FetchUserListOutput;
}
Output

返すデータの型と構造を厳密に定義します。ロジックを持たず、型がすべてを語ります。これは Data Transfer Object (DTO) と呼ばれます。具体例は以下のようになります。

<?php
/**
 * データ構造 (Output DTO)
 *
 * 役割: FetchUserListUseCase の戻り値の「型」。
 * "型が語る"ドキュメントであり、コメントは不要。
 * ロジックは一切持たない。
 */
final class FetchUserListOutput
{
    /**
     * @param UserOutputData[] $users ユーザー情報の配列 (下のクラスを参照)
     * @param int $totalCount 取得した総数
     */
    public function __construct(
        public readonly array $users,
        public readonly int $totalCount,
    ) {
    }
}

/**
 * データ構造 (Output DTO の一部)
 *
 * 役割: $users 配列の中身の「型」。
 */
final class UserOutputData
{
    public function __construct(
        public readonly string $id,
        public readonly string $displayName,
        public readonly string $joinedDateFormatted, // 例: '2025/10/28'
        public readonly bool $isPremiumUser,
    ) {
    }
}
実装クラス

Interface で約束した機能を実際に実装します。ロジック、データ加工、データ取得など、具体的な処理はすべてここで行います。具体例は以下のようになります。

<?php
/**
 * 実装クラス (実際の処理)
 *
 * 役割: FetchUserListUseCaseInterface の契約(handleメソッド)を実装する。
 * ビジネスロジックはここにカプセル化される。
 * このクラスは将来的に何度でも書き換え可能。
 */
class FetchUserListUseCase implements FetchUserListUseCaseInterface
{
    // 依存するのは Repository の「Interface」
    private UserRepositoryInterface $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * {@inheritdoc}
     * 契約(Interface)で決められた処理を実行する
     */
    public function handle(string $status): FetchUserListOutput
    {
        // 1. Repositoryからデータを取得 (これは生のDBデータやドメインモデル)
        $domainUsers = $this->userRepository->findByStatus($status);

        // 2. ビジネスロジックの適用
        // (例: 登録から1年以上経過したユーザーのみに絞り込む)
        $filteredUsers = array_filter($domainUsers, function ($user) {
            return Carbon::parse($user->created_at)->diffInYears(Carbon::now()) >= 1;
        });

        // 3. 「Output」のデータ構造に変換する
        $outputUsers = [];
        foreach ($filteredUsers as $user) {
            
            // (ロジック: '様' を付ける、日付をフォーマットする)
            $displayName = $user->name . '様';
            $joinedDate = Carbon::parse($user->created_at)->format('Y/m/d');
            $isPremium = $user->plan_type === 'premium';

            // DTOに詰める
            $outputUsers[] = new UserOutputData(
                id: (string) $user->id,
                displayName: $displayName,
                joinedDateFormatted: $joinedDate,
                isPremiumUser: $isPremium
            );
        }

        // 4. 最終的な「FetchUserListOutput」の型で返却する
        return new FetchUserListOutput(
            users: $outputUsers,
            totalCount: count($outputUsers)
        );
    }
}
なぜ3ファイルに分けるのか?

UseCase層がRepository層を呼び出す実装において、普通の実装を行うと、高レベルのモジュール(UseCase)が、低レベルのモジュール(Repository)の実装を知っている状態、すなわちUseCase層がRepository層に依存する状態になります。これは、ビジネスロジック(UseCase)が、インフラ(DB)の都合によって変更されてしまう、脆い(もろい)設計といえます。このような状態を解消するためには、抽象(Interface)を用いた設計が有効です。高レベルモジュールが低レベルモジュールのInterfaceのみを見て実装を行うことによって、高レベルモジュールが低レベルモジュールの実装に依存することを避けることができます。この時そのInterfaceを実装するのは低レベル層であるため、普通の実装における「高レベル層→低レベル層」の実装の依存関係から、「低レベル層→抽象(高レベル層が参照)」の実装の依存関係に変わります。このことを「依存性の逆転」と呼びます。

達成した業務内容

コラム詳細ページについて、Controller層、UseCase層、Repository層、ViewModel層、View層をすべて実装し、テストの作成・実行を行いました。

学んだこと・得られた経験

PHP、Laravelの業務経験

私は、本インターン参加前の時点で他の言語の開発経験はあったのですが、PHPは完全未経験でした。そのため、一からキャッチアップしながら業務に従事しました。業務で扱ったアプリケーションは規模が大きく、使用しているアーキテクチャも複雑な構造を持っていたので、キャッチアップに時間がかかってしまい、序盤のうちはなかなか思ったような実装を行うことができませんでした。そんな中でも担当社員の方が丁寧にアプリケーションについて教えてくださったおかげもあり、少しずつ理解が進んでいき、実装を行うことができました。

この、完全未経験の言語での開発を一からキャッチアップして、大規模アプリケーションへの実装を行うことができた経験というのは、自分の中で非常に大きなものになったのではないかと感じています。

リアルなエンジニア業務経験

インターンではかなりエンジニアのリアルに近い実務経験を積めたのではないかと感じています。その理由は以下の通りです。

  • 実際に社内で開発しているアプリケーションの実装を行えた
  • 社内規定に従ってプルリクエストを出し、社員の方からレビューをいただいて修正する経験を積めた
  • 開発チームの週次ミーティングに参加して、自分も開発チームの一員として進捗報告を行い、他の人の進捗報告を聞くという経験を積めた

インターンでの業務を経て、「エンジニアとして働く」ということの自分の中のイメージがより具体化されたと考えています。

おわりに

1ヶ月の間、 "Booost!!! Excite Internship 2025" で貴重な経験を積ませていただきありがとうございました!PHP未経験の私を一から導いて下さった担当社員の方をはじめ、私を受け入れてくださった社員の皆様に感謝申し上げます。