インターンでデリッシュキッチンの新機能開発に取り組みました – every Tech Blog

1. はじめに

こんにちは、everyで1ヶ月間のインターンシップに参加させていただいた宮田です。本記事では、デリッシュキッチンの新機能開発に携わった経験と、そこで得られた学びを紹介します。
現在、デリッシュキッチンの既存仕様に対して、ユーザー体験を向上させるための新しい機能開発を進めています。今回のタスクでは、ユーザーをグループ化する新機能のバックエンドAPI実装を担当しました。

2. プロジェクト全体像と技術スタック

デリッシュキッチンサーバーの概要は下の図のようになっています。ダッシュボード側ではユーザー情報の管理・監視を行い、モバイルアプリ側ではデータベースからリモートキャッシュにセットした情報をユーザー管理画面に表示します。詳細はDELISH KITCHENのシステムアーキテクチャ
で説明しています。今回は、ユーザーをグループ化するAPIとそのグループに招待するコード作成・取得機能を中心としたAPIを実装しました。

技術スタック

  • バックエンド: Go (Echo)
  • データベース: MySQL
  • リモートキャッシュ: Redis

3. デリッシュキッチンサーバー・バックエンド実装

デリッシュキッチンのバックエンドはクリーンアーキテクチャで構成されています。クリーンアーキテクチャとは、ビジネスロジックを外部のフレームワークやツールから切り離すことで、保守性・拡張性を高める設計手法です。主にrepository、infrastructure、service、handler、routerの5つの階層を用いています。最近では、多くの企業で標準的に採用されているようですが、私は今回が初めての経験だったため、概念の理解やコード分割に苦戦しました。

infrastructure・repository

infrastructureは、外部システムとの接続やデータの永続化を担当する層です。repositoryは、データアクセスロジックを抽象化し、ビジネスロジックからデータベースの実装詳細を隠蔽する役割を持ちます。この2つによって、データベース操作の詳細をビジネスロジックから分離し、テスタビリティと保守性を向上させています。

今回のグループ機能実装では、グループの作成・招待コード生成・招待コード取得のためのリポジトリインターフェースを定義し、MySQL用の実装を作成しました。

func (r *InvitationCodeRepository) CreateTx(ctx context.Context, tx dbr.SessionRunner, m *model.InvitationCode) (*model.InvitationCode, error) {
    result, err := tx.InsertInto(r.getTable()).
        Columns("group_id", "invitation_code", "expires_at", "is_active").
        Record(m).
        Exec()
    if err != nil {
        return nil, e.Wrap(err, "couldn't create invitation code")
    }

    id, err := result.LastInsertId()
    if err != nil {
        return nil, e.Wrap(err, "couldn't get last insert id")
    }

    m.ID = id
    return m, nil
}

データベースへのINSERT操作をトランザクション内で実行しています。招待コード作成ではグループとの関連(group_id)と状態管理(is_activeexpires_at)を含めたレコードを作成しています。LastInsertId()で生成されたIDを取得してモデルに設定し、エラー処理はpkg/errorsパッケージでラップして詳細な情報を保持しています。

service

serviceは、ビジネスロジックを実装する層で、repositoryを通じて取得したデータに対して業務要件を満たす処理を行います。複数のrepositoryを組み合わせて複雑な処理を実現し、トランザクション管理も担当します。

今回の実装では、グループへの招待コード自動生成や、招待コード取得時のアクセス権限チェックなどのビジネスルールを実装しました。特に招待コードは、セキュリティを考慮してランダム文字列生成と有効期限設定を行っています。

func (s *InvitationCodeServiceImpl) CreateInvitationCode(ctx context.Context) (*model.InvitationCode, error) {
    
    session := db.GetSession("t3")
    tx, err := session.Begin()
    if err != nil {
        return nil, e.Wrap(err, "failed to begin transaction")
    }
    defer tx.RollbackUnlessCommitted()

    
    _, err = s.invitationCodeRepo.DeactivateByGroupIDTx(ctx, tx, group.ID)
    if err != nil {
        return nil, e.Wrap(err, "failed to deactivate existing invitation codes")
    }

    
    code, err := random.GenerateInvitationCode()
    if err != nil {
        return nil, e.Wrap(err, "failed to generate invitation code")
    }

    
    expiresAt := time.Now().Add(24 * time.Hour)

    newInvitationCode := model.NewInvitationCode(group.ID, code, expiresAt)
    createdInvitationCode, err := s.invitationCodeRepo.CreateTx(ctx, tx, newInvitationCode)
    if err != nil {
        return nil, e.Wrap(err, "failed to create invitation code")
    }

    if err := tx.Commit(); err != nil {
        return nil, e.Wrap(err, "failed to commit transaction")
    }

    return createdInvitationCode, nil
}

serviceレイヤーでは、複数のリポジトリを組み合わせたビジネスロジックを実装しています。招待コード生成では、トランザクション管理下で既存コードの無効化と新規コード生成を一貫して行い、ACID特性を保証しています。また、セキュリティ面では24時間の有効期限設定やランダム文字列生成を行い、システムの安全性を確保しています。

handler・router

handlerは、HTTPリクエストを受け取り、リクエストデータの検証、serviceの呼び出し、レスポンスの組み立てを行う層です。routerは、URLパスとHTTPメソッドに基づいて適切なhandlerにリクエストを振り分ける役割を担います。この2つで、外部からのAPIリクエストを適切に処理し、JSONレスポンスを返すWebAPIを実現しています。

今回は、グループ作成・招待コード生成・招待コード取得の3つのエンドポイントを実装しました。各エンドポイントでは、リクエストパラメータのバリデーション、認証チェック、エラーハンドリングを適切に行っています。

func (h *InvitationCodeHandlerImpl) CreateInvitationCode(c echo.Context) error {
    user := h.userAuth.GetUser(c)
    if user == nil {
        return types.ErrNotAuthorized
    }

    invitationCodeModel, err := h.invitationCodeService.CreateInvitationCode(dctx.NewUserContext(c))
    if err != nil {
        return err
    }

    invitationCodeResponse := response.NewInvitationCode(invitationCodeModel)
    return JSONHTTPSuccessHandlerAsMap("invitation_code", invitationCodeResponse, c)
}

handlerレイヤーでは、HTTPリクエストを受け取ってserviceレイヤーに処理を委譲し、適切なJSONレスポンスを返しています。全てのエンドポイントで共通して認証チェック(userAuth.GetUser())を実行し、未認証の場合はErrNotAuthorizedエラーを返しています。また、dctx.NewUserContext() でユーザー情報をコンテキストに埋め込み、service層でユーザー固有の処理ができるようにしています。レスポンス生成では、統一的なフォーマット(JSONHTTPSuccessHandlerAsMap)を使用してクライアントに一貫した形式でデータを返すよう設計されています。

4. インターンシップを通じて学んだこと

GoとTypeScriptの比較

今回初めてGoを使用して開発を行ったため、書き方や仕様を把握するのが大変でした。普段はTypeScriptを使用することが多いのですが、Goを触ったことで以下のような気づきを得ました。

型の違い

TypeScriptでは柔軟で表現力が高いのに対し、Goはシンプルで設計の曖昧さを許さないという違いがあります。ポインタやスライス設計を意識せざるを得ない点は新鮮でした。

非同期処理とcontext

TypeScriptはPromise/async-awaitが主流ですが、Goはcontext.Contextで処理のライフサイクルを統一的に管理できます。これは信頼性を高める強力な仕組みだと実感しました。

テスト文化

Jestでの振る舞いテストが中心のTypeScriptに比べて、Goは層ごとの責務を意識してモックを徹底的に利用します。特に、データベースに直接触らずにテストするという設計方針は強く印象に残りました。

実装について

アーキテクチャ設計とパフォーマンス

今回の実装では、プロジェクトのコーディング規約に従った型設計の重要性を学びました。例えば、スライス型の設計では[]Typeではなく[]*Typeを使用することで、パフォーマンス向上とコードベース全体の一貫性を保つことができます。また、クリーンアーキテクチャにおける依存関係の管理では、定義されていない方法での依存が発生しないよう、各層の責務を明確に分離することが重要でした。

トランザクション管理とエラーハンドリング

データベース操作では、単体の関数とトランザクション版の関数を分離し、前者は後者を呼び出すだけにしてメインロジックは後者に集約する設計パターンを学びました。エラー処理では、==ではなくerrors.Is()を使用した適切な比較や、typesパッケージで定義された標準エラーの活用により、一貫性のあるエラーハンドリングを実現できました。

コード効率性とパフォーマンス最適化

実装時には、早期returnの活用やfor-rangeでの要素検索における標準パッケージslicesの使用など、効率的なロジック設計を心がけました。また、無駄なDBアクセスを避けるためのロジック設計や、ORMのLoadOneメソッドを適切に使用することで、パフォーマンスの向上を図りました。

エラー処理の考え方

TypeScriptのtry-catchに比べ、Goは戻り値で明示的にerrorを返すため、どこで失敗する可能性があるかが明確に見えます。特に、infrastructure層でwrapしたエラーをservice層で再度wrapするかどうかの判断や、エラーの発生源を意識したスタックトレース設計の重要性を学びました。

コーディング規約と命名規則

変数名とコメントの適切性

実装時には、変数名がデータベースのカラム名や既存のプロジェクト慣習に則っているかを常に確認することの重要性を学びました。また、コードを読めば分かる内容についてはコメントを書かず、本当に必要な説明のみをコメントとして残すことで、コードの可読性を向上させることができました。

関数の命名と設計

新しい機能を作成する際には「Add」ではなく「Create」を使用するなど、既存のコードベースの命名規則に従うことの重要性を実感しました。また、使用されていないinterfaceや関数定義は削除し、コードベースをクリーンに保つことも大切だと学びました。

テスト実装

モックの活用とテスト設計

単体テストでは実データベースを使用せず、下位のservice/repositoryにはモックを使用することで、テストの独立性と実行速度を確保できました。テストケース作成時には「このテストで何が検証できているのか」を常に意識し、冗長なテストを避けることの重要性を学びました。

並列テストとテストケース設計

DBアクセスを行わないテストではt.Parallel()を使用した並列化を必ず行い、テスト実行時間を短縮しました。また、全てのエラーパターンを網羅的にテストケースに含め、特にgomock.Any()ではなく具体的な型での検証を行うことで、より堅牢なテストを実現できました。

プルリクエストとレビュー文化

プルリクエスト作成時の配慮

PR作成時には、将来のタスクで使用予定の実装でも、今回のPRに関係ない部分はレビューの邪魔になるため除外することの重要性を学びました。また、テストが落ちている状態でPRを作成しないよう、事前にテストを実行して通った状態にしておくことも基本的なマナーだと感じました。

レビュー可能なPRの作成

プロジェクトに関わっていないレビュアーでもレビューできるよう、PRのdescriptionには初見では分からない情報や背景を丁寧に記載することの大切さを実感しました。これにより、チーム全体での知識共有とコードの品質向上に貢献できます。

レビュー文化

レビューの返ってくるスピードの早さに驚きました。レビューをしないと他の人の作業を止めてしまう、また、人のコードを客観的に見ることで自分も勉強になるから優先的にレビューを行うという考え方が非常に良いと思い、ぜひ自分も真似していきたいと感じました。モックの生成コマンドをMakefileに追加するなど、チーム開発での協調性を意識した細かい配慮も重要だと学びました。

5. まとめ

1ヶ月間のインターンシップを通して、デリッシュキッチンのグループ機能という大事な新機能実装を任せていただいて非常に貴重な体験となりました。普段行っているWeb開発では体験できないテストやCI/CDの自動化ツールであったり、リリース作業などを体験させていただけました。これまで概念として知っていたデータベースのインデックスやトランザクションなど、実際に自分の知識を初めてコードに反映することができてよかったです。また、細かくレビューしていただいたことで、商用としてのより良い実装だけでなく、社内の実装ルールやPR作成時に気をつけなければいけないことなど、自分の中に今までなかった様々なことを学ばせていただきました。今回のインターンシップ参加を通して、従業員として業務をこなしたことによる新しい発見や成長を得ることができ、自分がこれから勉強するべきことなども見つけることができました。また、どれだけ既存コードが理解できていなくてわからない状態でも、実装や開発は非常に楽しいなと常に思っていたので、改めて自分が開発が好きだということを再確認できてよかったです。これからは、今回の実装で学んだことやレビューいただいた内容を元に、どんどん成長して、より良いエンジニアになっていきたいです。




Source link

関連記事

コメント

この記事へのコメントはありません。