[Architecture] Layerd Architecture & DI & Unit Test


Project Description
今日、KNOWMARKプロジェクトの再編過程でLayed Architectureを適用し、DIを通じてUnit Testを適用します.プロジェクトで使用される主なスタックは次のとおりです.
  • Language: Node.js, Typescript
  • Framework: Express.js
  • ORM: TypeORM
  • では、包装を直す前に汚れたAPIの例を見てみましょう.あ参考にリニューアル前に使っていたフレームワークはKoajsを使用したことがあります.
    再包装前のgetPosts API
    export const getPosts = async (ctx: Context) => {
      let body, status: number, posts;
      const { page } = ctx.request.query;
      const start = (Number(page) - 1) * 15;
      const totalNums = await getConnection().getRepository(Post).createQueryBuilder('post').select('COUNT(*) AS cnt').getRawMany();
      const canHaveMaxPage: number = Math.ceil(Number(totalNums[0].cnt) / 15);
      const withoutcomplete = ctx.header.withoutcomplete === '1' ? true : false;
    
      if (Number(page) > canHaveMaxPage) {
        ctx.status = 400;
        ctx.body = await errorCode(601, `글이 더이상 존재하지 않습니다. 마지막 페이지는 ${canHaveMaxPage}페이지 입니다.`);
        return;
      }
    
      if (!page || start < 0) {
        status = 400;
        body = await errorCode(401);
      } else {
        if (withoutcomplete) {
          posts = await getConnection()
            .getRepository(Post)
            .createQueryBuilder('post')
            .leftJoinAndSelect('post.medias', 'media')
            .orderBy('post.isArchived', 'ASC')
            .addOrderBy('post.createDate', 'DESC')
            .offset(start)
            .limit(15)
            .getMany();
        } else {
          posts = await getConnection().getRepository(Post).createQueryBuilder('post').leftJoinAndSelect('post.medias', 'media').orderBy('post.createDate', 'DESC').offset(start).limit(15).getMany();
        }
    
        posts.forEach((element) => (element.user = { uid: element.createUserUid }));
    
        status = 200;
        body = posts;
      }
    
      ctx.compress = true;
      ctx.status = status;
      ctx.body = body;
      ctx.set('Content-Type', 'application/json');
    };
    簡単な記事リストを検索するAPIです.1つのAPIで要求を受信して応答し、ビジネスロジックに基づいて応答状態とbody状態を決定し、DBからデータにアクセスする.
    今から考えてみると、これはどんなに汚くて、煩雑で、相手のコードに関心がありません...すべてのAPIがこのように記述されている.したがって、プロジェクトの再構築では、これらの汚れたコードを各ロールから分離してテストするのに適したコードを作成することが第一のタスクです.
    再構築されたコードを表示する前に、適用するレイヤアーキテクチャについて説明します.
    Layerd Architecture
    階層アーキテクチャは最も一般的なアーキテクチャモデルであり,n層アーキテクチャモデルと呼ばれる階層アーキテクチャモデルである.このモードはほとんどのJava EEアプリケーションの事実上の基準であるため、Springをよく知っている開発者であれば、知らないことも知らないこともありません.アーキテクチャに適した方法でコードを作成した可能性があります.
    階層アーキテクチャでは、各階層がアプリケーションで特定の役割を果たします.階層型アーキテクチャ・モードでは、モデルに存在する必要があるレイヤの数とタイプは指定されませんが、ほとんどの階層型アーキテクチャは、プレゼンテーション、ビジネス、持続性、データベースの4つの標準レイヤで構成されています.ただし、より小さなアプリケーションは3つのレイヤしかない場合がありますが、より複雑なアプリケーションは5つ以上のレイヤを含む場合があります.これは、プロジェクトの規模によって階層が増加したり、減少したりする可能性があることを意味します.つまり「4つの階層に行かなければならない!そうではありません.

    この階層アーキテクチャでは、各階層が特定の役割と責任だけを実行することが重要です.たとえば、プレゼンテーション・レイヤは、すべてのユーザ・インタフェースおよびブラウザ通信ロジックを処理し、ビジネス・レイヤは、要求に関連する特定のビジネス・ルールを実行します.すなわち,各レイヤは,特定の動作を満たすために実行される操作を中心として,他の動作を抽象化する.たとえば、プレゼンテーション・レイヤは、顧客データの取得方法を知ったり心配したりする必要はありません.知らないはずがない特定のフォーマットの画面に情報を表示することは、プレゼンテーションレイヤの職責と責任です.
    階層アーキテクチャモードの利点は,階層間の関心が分離されることである.興味分離は複数の階層を意味するが、簡単に言えば、1つの階層のコード変更は別の階層のコードに影響を与えない.したがって、階層型アーキテクチャ・モードにより、アプリケーションの拡張、テスト、メンテナンスが容易になります.
    もっと詳しく知りたい場合は、対応する文章を参照してください.
    階層アーキテクチャの適用後
    では今、Refactoring後のコードを見てみましょう例はgetPosts APIでもある.
    Controller Layer
    コントローラ層は、クライアントからHttp要求を受信し、適切なHttp応答に応答するだけである.ここで、作成者から入力されたPostServiceは、ビジネスロジックに従って適切なデータを取得する.しかし、コントローラ層では、そのサービス層で起こったことに興味がなく、知らない.残りの階層は同じだ.
    async showPosts(req: Request, res: Response, next: NextFunction) {
        const { last_id } = req.query;
        const [posts, nextLastId] = await this.postService.getPosts(String(last_id));
    
        return {
            statusCode: 200,
            response: {
                posts,
                nextLastId
            }
        };
    }
    Service Layer
    サービス・レベルは、ビジネス・ロジックに基づいてデータの照会と加工を担当します.同様に、作成者が入力したPostRepositoryから必要なデータを取得します.
    async getPosts(lastId: string) {
        let posts;
    
        if (lastId !== 'null') {
          posts = await this.postRepository.getPosts(Number(lastId));    
        } else {
          posts = await this.postRepository.getPostsForFirstPage();
        }
        
        const convertedPosts = this.convertPostHaveOneImage(posts);
    
        if (convertedPosts.length < 20) {
          return [convertedPosts, null];
        } else {
          return [convertedPosts, convertedPosts[convertedPosts.length - 1].id];
        }
      }
    Repository Layer
    Knowledge Baseレイヤでは、データベースにアクセスしてデータにアクセスするだけで責任と役割を果たすレイヤです.最後の層なので、他に注入される対象はありません.
    getPosts(lastId: number) {
        return this.createQueryBuilder('p')
          .select(['p.id', 'p.title', 'p.created_at', 'i.url'])
          .leftJoin('p.images', 'i')
          .where('p.id < :lastId', { lastId })
          .orderBy('p.id', 'DESC')
          .limit(20)
          .getMany();
      }
    
      getPostsForFirstPage() {
        return this.createQueryBuilder('p')
          .select(['p.id', 'p.title', 'p.created_at', 'i.url'])
          .leftJoin('p.images', 'i')
          .orderBy('p.id', 'DESC')
          .limit(20)
          .getMany();
      }
    再包装する前のコードよりずっときれいではないでしょうか.単純なコードの簡潔性を超えて、本当のメリットはテストです.
    Unit Test
    単位テストについてはあまり言いません.ユニットテストは、アプリケーションでテスト可能な最小のソフトウェアを実行して、予想通りに実行されるテストを確保します.ユニットテストでは、テスト対象ユニットのサイズは厳密に規定されていませんが、通常はクラスまたはメソッドレベルです.
    メモリ層のセルテストのためにSQLiteというメモリDBMSを用いて簡単なテストを行った.LinuxやMacなどのUnixシリーズのOSは基本的にインストールされている可能性があります.参考にしてください.
    Jestをテストライブラリとして使用します.詳細コード省略!これはJestライブラリに関する文章ではないからです.
    Repository Layer
    getPostsForFirstPageクエリーのテスト
    it('getPostsForFirstPage - 첫 페이지 글 목록 조회', async () => {
        // given
        const title = '치피 파티 할 사람??';
        const description = '치킨나라 피자공주 같이 시켜먹어요!! 너무 심심해요..ㅜㅜ';
        const location = 2;
        const max_head_count = 4;
        
        for (let i = 0; i < 15; i++) {
            await postRepository.insertPost(title, description, location, max_head_count);
        }
    
        // when
        const result1 = await postRepository.getPostsForFirstPage();
        
        // then
        expect(result1.length).toBe(15);
      });
    getPostsクエリーのテスト
    it('getPosts - 페이징을 통한 글 목록 조회', async () => {
        // given
        const title = '치피 파티 할 사람??';
        const description = '치킨나라 피자공주 같이 시켜먹어요!! 너무 심심해요..ㅜㅜ';
        const location = 2;
        const max_head_count = 4;
        
        for (let i = 0; i < 43; i++) {
            await postRepository.insertPost(title, description, location, max_head_count);
        }
    
        // when
        const result1 = await postRepository.getPosts(24);
        const result2 = await postRepository.getPosts(4);
        
        // then
        expect(result1.length).toBe(20);
        expect(result1[0].id).toBe(23);
        expect(result1[19].id).toBe(4);
        
        expect(result2.length).toBe(3);
        expect(result2[0].id).toBe(3);
        expect(result2[result2.length - 1].id).toBe(1);
      });
    基本的に両方ともクエリーなので、事前にデータを入れて所望の結果を得ることができます.

    Service Layer
    getPostsメソッドテスト
    describe('getPosts - 공동구매 글 조회', () => {
        it('성공 - lastId가 null인 경우 첫 번째 페이지의 글 조회', async () => {
          // given
          const lastId = 'null';
    
          // when
          postRepository.getPostsForFirstPage.mockResolvedValue([]);
          await postService.getPosts(lastId);
    
          // then
          expect(postRepository.getPostsForFirstPage).toHaveBeenCalled();
        });
    
        it('성공 - lastId가 number인 경우 lastId보다 작은 수부터 20개 조회', async () => {
            const lastId = '22';
    
            // when
            postRepository.getPosts.mockResolvedValue([]);
            await postService.getPosts(lastId);
      
            // then
            expect(postRepository.getPosts).toHaveBeenCalled();
        });
      });
    サービス層には、注入されたストレージ層を介してデータにアクセスする部分があるため、これらの部分を偽の行為に偽装する必要がある.ユニットテストは、テスト対象の役割と責任のみをテストするため、ビジネスロジックに基づいてデータ加工を担当するサービス層では、ビジネスロジックに基づいてロジックにエラーがあるかどうかをテストするだけでよい.なぜなら、リポジトリ・レイヤのデータ・アクセス部分として責任がないからです.

    整理する
    要するに結論として、私が感じているのは:クリーンなコードと同僚のためにも、アプリケーションの拡張、テスト、メンテナンスのためにも、適切なアーキテクチャが必要です.私の場合、階層化アーキテクチャを採用して再設計することで、各階層は単一の責任で各階層での役割を果たすことができます.テスト環境はずっと簡単になりました.また、各レイヤに問題が発生した場合は、そのレイヤのコードを変更するだけでよいので、メンテナンスが容易なシステムになっているようです.これは簡単な作業ではありませんが、システムを交換した後、私はとても喜んでいます.