자바스크립트 공부 일지 22

Layered Architecture Pattern

Layered Architecture Pattern

도메인

  • 개발자 대부분은 비즈니스 프로세스를 개선하거나 자동화하기 위해 일한다. 도메인은 이런 프로세스가 지원하는 활동을 의미한다.
  • 가구 판매 회사의 도메인은 구매 및 조달, 제품 설계, 물류 및 배달 등 다른 분야를 뜻할 수 있습니다.

도메인 (Domain)의 예시

  • 개발자 입장에서 온라인 서점을 구현해야 할 소프트웨어의 대상이 됩니다.
    • 온라인 서점 = 소프트웨어로 해결하고자 하는 문제의 영역 = 도메인 (Domain)
  • 도메인은 다시 하위 도메인으로 나눌 수 있습니다.
    • 도메인 구성 : 주문, 회원, 혜택, 결제, 배송, 정산, 카탈로그, 리뷰
    • 하위 도메인다른 하위 도메인과 연동하여 완전한 기능을 제공합니다.
      • ex) 고객 → 물건 주문 → 결제 → 배송 → 혜택 제공
  • 하위 도메인을 어떻게 구성할 지 여부는 상황에 따라 달라집니다.
    • B2B(Business-to-Business), B2C(Business to Consumer)

도메인 모델

  • 도메인 모델이란 유용한 특성을 포함하는 프로세스나 현상의 지도(Map)를 뜻합니다. 도메인 모델은 비즈니스를 수행할 사람이 자신의 비즈니스에 대해 마음속에 가지고 있는 지도와 같습니다.

  • 도메인 모델이란 특정 도메인을 개념적으로 정리한 모델입니다.

  • 도메인 모델은 사용할 개체를 기억하기 쉬운 이름(식별자)을 부여해 대상을 쉽게 공유할 수 있게 한다.

  • 만약 누군가가 저희에게 공을 던진다면 우리는 무의식적으로 이 공의 움직임을 예측해 회피할 것입니다.

    • 우리들의 머릿속에 이미 공간상 물체가 움직이는 방식에 대한 모델이 있어서 움직임을 무의식적으로 예측할 수 있습니다.

도메인 모델링 (Domain Modeling)의 종류

  1. 엔티티 (Entity)
  2. 값 객체 (Value object)
  3. 도메인 서비스 (Domain service)

엔티티

  • 엔티티(Entity)는 실제 DB 테이블과 연관되어 있는 핵심 클래스이고, 엔티티를 기준으로 테이블이 생성되고 DB 스키마가 변경됩니다.
    ex) Sequelize Models

  • 엔티티를 **요청(Request)**이나 **응답값(Response)**으로 전달하는 클래스로 사용하면 안됩니다.

  • 엔티티 내부의 속성변경되더라도 여전히 동일한 엔티티로 남아있습니다.

  • 엔티티는 시간에 따라 변하는 속성포함될 수 있습니다.

  • 어떤 요소가 엔티티유일하게 식별하는지 정의하는 것 또한 중요합니다.

    • 보통 이름이나 참조 번호 등을 사용합니다.
    • ex) User 클래스의 userId

아키텍처 패턴

  • 아키텍처 패턴은 소프트웨어의 구조를 구성하기위한 가장 기본적인 토대를 제시합니다.
  • 아키텍처 패턴은 각각의 시스템들과 그 역할이 정의되어 있고, 여러 시스템 사이의 관계규칙 등이 포함되어 있습니다.
  • 검증된 구조로 개발을 진행하기 때문에 안정적인 개발이 가능합니다.
  • 아키텍처 패턴을 도입할 경우 도메인이 복잡할수록 모델이나 코드를 더 쉽게 변경할 수 있다는 측면에서 큰 이익을 얻을 수 있습니다.

대표적인 아키텍처 패턴

  • 저장소 패턴 (Repository pattern)

    • 영속적인 저장소에 대한 추상화
  • 서비스 계층 패턴 (Service layer pattern)

    • 유스 케이스(Usecase)의 시작을 명확하게 정의하기 위한 패턴
  • 작업 단위 패턴 (Unit of work pattern)

    • 원자적 연산을 제공합니다.
  • 애그리게이트 패턴 (Aggregate pattern)

    • 데이터 정합성을 강화하기 위한 패턴
  • 아키텍처 패턴을 도입하기 전에 고민해야 할 것

    1. 아키텍처 패턴이 주는 이익비용에 대해 확실한 이유가 있어야합니다.
    2. 해당하는 아키텍처 패턴을 채택했을 때 어떤 장단점이 존재하는지 명확하게 인지해야 합니다.
    3. 여러 계층을 추가하기 위해 들이는 노력시간을 투자할 만한 가치가 있을 정도로 어플리케이션도메인복잡한 경우에만 아키텍처 패턴을 도입해야 합니다.

계층형 아키텍처 패턴

  • 계층형 아키텍처 패턴(Layered Architecture Pattern)은 계층을 분리해서 관리하는 아키텍처 패턴이고, 현재 가장 흔하게 사용되고 있는 아키텍처 패턴 중 하나입니다. 단순하고 대중적이면서 비용도 적게 들어 모든 어플리케이션의 사실상 표준 아키텍처입니다.

어떤 아키텍처 패턴을 도입할지 확신이 없을 때에는 계층형 아키텍처 패턴은 좋은 선택지가 될 수 있습니다.

계층형 아키텍처 패턴은 어떤 경우든 계층을 분리해서 유지하고, 각 계층이 자신의 바로 아래 계층에만 의존하게 만드는 것이 목표입니다.

계층화의 핵심은 각 계층은 응집도(Cohesion)가 높으면서, 다른 계층과는 낮은 결합도(Coupling)를 가지고 있어야합니다.

여기서 상위 계층은 하위 계층을 사용할 수 있지만, 하위 계층은 자신의 상위 계층에 누가 있는지 알 수 없고, 사용할 수 조차 없도록 구성해야합니다.

일반적으로 계층형 아키텍처 패턴의 경우 규모가 작은 어플리케이션의 경우 3개, 크고 복잡한 경우는 그 이상의 계층으로 구성됩니다.

저희가 알아볼 아키텍처 패턴은 **3계층 아키텍처(3-Layered Architecture)**입니다.

3계층 아키텍처에서 구성되는 각각의 **계층(Layer)**는 아래와 같습니다.

  • 프레젠테이션 계층 (Presentation Layer)
  • 비즈니스 로직 계층 (Business Logic Layer)
  • 데이터 엑세스 계층 (Data Access Layer) | 영속 계층(Persistence Layer)

아키텍처 패턴

  • 계층형 아키텍처 패턴의 장점 저희는 이렇게 계층별로 아키텍처를 분리했을때 아래의 장점을 얻을 수 있습니다.

    • 관심사를 분리하여 현재 구현하려하는 코드명확하게 인지할 수 있습니다.
    • 각 계층별로 의존성이 낮아 모듈교체하더라도 코드 수정이 용이합니다.
    • 각 계층별로 단위 테스트를 작성할 수 있어 테스트 코드를 조금 더 용이하게 구성할 수 있습니다.
  • 3계층 아키텍처 (3-Layered Architecture)

    3-Layered Architecture는 아래의 3가지의 처리과정을 이용해서 구현합니다.

    1. Controller : 어플리케이션의 가장 바깥 부분, 요청/응답을 처리함.
      • 클라이언트의 요청을 처리 한 후 서버에서 처리된 결과반환해주는 역할을 합니다.
    2. Service : 어플리케이션의 중간 부분, 실제 중요한 작동이 많이 일어나는 부분
      • 아키텍처의 가장 핵심적인 비즈니스 로직이 수행되는 부분입니다.
    3. Repository : 어플리케이션의 가장 안쪽 부분, DB와 맞닿아 있음.
      • 실제 데이터베이스의 데이터를 사용하는 계층입니다.

3-Layered Architecture에서는 아래의 플로우를 기반으로 로직이 수행됩니다.

  1. **클라이언트(Client)**가 **요청(Request)**을 보냅니다.
  2. **요청(Request)**을 URL에 알맞은 컨트롤러(**Controller)**가 수신 받습니다.
  3. 컨트롤러(**Controller)**는 넘어온 요청처리하기 위해 **서비스(Service)**를 호출합니다.
  4. **서비스(Service)는 필요한 데이터를 가져오기위해 저장소(Repository)**에게 데이터를 요청합니다.
  5. **서비스(Service)**는 **저장소(Repository)**에서 가져온 데이터를 가공하여 컨트롤러(**Controller)**에게 데이터를 넘깁니다.
  6. 컨트롤러(**Controller)**는 **서비스(Service)**의 **결과물(Response)**을 **클라이언트(Client)**에게 전달해줍니다.
  • 👉 서버 개발자들은 서버에서의 처리과정이 대부분 비슷하다는 걸 깨닫고, 처리 과정을 크게 3개로 분리하였고 Controller, Service, Repository 입니다. 각 역할 별로 하는 일을 정리해 보겠습니다.

Controller

컨트롤러

  • 클라이언트의 요청을 받음
  • 요청에 대한 처리는 서비스에게 전담
  • 클라이언트에게 응답

Service

서비스

  • 사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세!
    • 현업에서는 서비스 코드가 계속 비대해짐
  • DB 정보가 필요할 때는 Repository 에게 요청

Repository

리포지토리

  • DB 관리 (연결, 해제, 자원 관리)
  • DB CRUD 작업 처리

최종적으로 다음과 같은 그림이 됩니다.

3way architecture

구현

컨트롤러 (Controller)

  • 컨트롤러(Controller)란 클라이언트의 요청을 처리 한 후 서버에서 처리된 결과를 반환해주는 역할을 합니다.

**컨트롤러(Controller)**는 추가적으로 아래의 역할을 가지고 있습니다.

  • 클라이언트의 요청(Request)을 수신합니다.
  • 요청(Request)에 들어온 데이터 및 내용을 검증합니다.
  • 서버에서 수행된 결과를 클라이언트에게 반환(Response)합니다.

프레젠테이션 계층(Presentation Layer) 이란?

  • 3계층 아키텍처 패턴에서는 프레젠테이션 계층(Presentation Layer)이라고 표현되며, 대표적으로는 컨트롤러(Controller)로 사용됩니다. 사용자(클라이언트)가 서버에 요청(Request)를 하게되면 가장 먼저 만나게 되는 계층입니다.

  • **하위 계층(서비스 계층, 저장소 계층)**에서 발생하는 **예외(Exception)**를 처리 합니다.

  • 클라이언트가 전달한 데이터에 대해 유효성을 검증하는 기능을 수행합니다.

  • 클라이언트의 요청을 처리한 후 서버에서 처리된 결과를 반환합니다.

Express로 구현하는 컨트롤러

컨트롤러

  • 클라이언트의 요청을 받음
  • 요청에 대한 처리는 서비스에게 전담
  • 클라이언트에게 응답

위에서 배운 내용을 바탕으로 컨트롤러(Controller)를 구현할 것입니다. 프로젝트를 시작하기 전 3계층 아키텍처 프로젝트에서 어떤 API를 만들지 확인해보겠습니다.

Express에서는 컨트롤러와 라우터를 연결하기 위해서 express.Router에서 특정 URI와 HTTP Method를 요청받았을 때 컨트롤러로 요청된 내용을 전달 해줘야합니다.

routes 폴더에서 posts.routes.js 라는 파일을 만들어 PostsConrtoller와 연결하도록 구성합니다.

js
// routes/posts.routes.js
const express = require('express');
const router = express.Router();

// 컨트롤러 모듈을 가져온다.
const PostsController = require('../controllers/posts.controller');
// 컨트롤러 모듈을 생성한다.
const postsController = new PostsController();

// get 메소드로 요청이 온 경우 포스트들을 보내준다.
// post 메소드로 요청이 온 경우 포스트를 생성한다.
router.get('/', postsController.getPosts);
router.post('/', postsController.createPost);

module.exports = router;
js
// controllers/posts.controller.js

const PostService = require('../services/posts.service');

// Post의 컨트롤러(Controller)역할을 하는 클래스
class PostsController {
  postService = new PostService(); // Post 서비스를 클래스를 컨트롤러 클래스의 멤버 변수로 할당합니다.

  getPosts = async (req, res, next) => {
    // 서비스 계층에 구현된 findAllPost 로직을 실행합니다.
    const posts = await this.postService.findAllPost();

    res.status(200).json({ data: posts })
  }

  createPost = async (req, res, next) => {
    const { nickname, password, title, content } = req.body;

    // 서비스 계층에 구현된 createPost 로직을 실행합니다.
    const createPostData = await this.postService.createPost(nickname, password, title, content);

    res.status(201).json({ data: createPostData });
  }
}

module.exports = PostsController;

await this.postService.findAllPost();PostsController 클래스에서 멤버 변수로 정의한 postService에서 findAllPost 메소드를 실행하는 코드입니다.

컨트롤러에서는 하위 계층이 어떠한 내부 구조를 가지고 있는지 신경쓰지 않고, 외부에 드러나 있는 메소드호출하기만 합니다. 이것이 가능한 이유는 **추상화(Absctraction)**의 특성 덕분입니다.

PostsController에서는 들어온 요청(Request)을 처리하기 위해 PostService를 호출하도록 구현하였습니다. 실제로 컨트롤러에서 비즈니스 로직을 수행하지 않고, 클라이언트의 **요청(Request)**을 바로 서비스 계층으로 전달 하도록 구현한 것을 확인 할 수 있습니다.

PostController에서는 클라이언트의 요청(Request)을 서비스 계층으로 데이터를 전달하는 것을 확인할 수 있습니다. 서비스 계층에서 어떠한 내부 구조를 통해 비즈니스 로직을 수행하는 것인지 상위 계층인 컨트롤러에게는 중요하지 않습니다.

Express로 구현하는 서비스

  • 서비스 계층(Service Layer)이란 비즈니스 로직 계층(Business logic layer)이라고도 불리는데요 아키텍처의 가장 핵심적인 비즈니스 로직을 수행하고 실제 사용자(클라이언트)가 원하는 요구사항을 구현하는 계층입니다.

  • **프레젠테이션 계층(Presentation Layer)**과 데이터 엑세스 계층(Data Access Layer) 사이에서 중간 다리 역할을 하며 서로 다른 두 계층이 직접 통신하지 않게 만들어 줍니다.

  • **서비스(Service)**는 데이터가 필요할 때 **저장소(Repository)**에게 데이터를 요청합니다.

  • 어플리케이션의 규모가 커지면 커질수록 서비스의 역할코드또한 점점 더 커지게 됩니다.

  • 어플리케이션의 핵심적인 비즈니스 로직을 수행하여 클라이언트들의 요구사항을 반영하여 원하는 결과를 반환해주는 계층입니다.

서비스 계층의 장점

각각의 **유스 케이스(Use Case)**와 **워크플로우(Workflow)**를 명확히 정의할 때 도움이 됩니다.

  • **저장소(Repository)**에게 얻을 필요가 있는 데이터가 무엇인지 이해할 수 있습니다.
  • 어떤 사전 검사현재 상태 검증을 필수적으로 해야하는 것인지 이해할 수 있습니다.
  • 어떤 내용을 저장해야 하는지 이해할 수 있습니다.

유스 케이스(Use Case)에 대해 자세히 알고 싶다면 여기를 클릭하세요!

  • 비즈니스 로직을 API 뒤에 감췄기 때문에 서비스 계층의 코드를 자유롭게 리팩터링할 수 있습니다.
  • 저장소 패턴(Repository Pattern) 및 **가짜 저장소(Fake Repository)**와 조합하면 높은 수준테스트작성할 수 있습니다.

서비스 계층의 단점

  • 서비스 계층또한 다른 추상화 계층에 불과합니다.
  • 서비스 계층에 너무 많은 기능을 넣으면 **빈약한 도메인 모델(Anemic Domain Model)**과 같은 안티 패턴이 생길 수 있습니다.
구현

Service

서비스

  • 사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세!
    • 현업에서는 서비스 코드가 계속 비대해짐
  • DB 정보가 필요할 때는 Repository 에게 요청

우리는 이전에 PostsControllerPostService에게 클라이언트의 **요청(Request)**을 전달하는 것을 확인하였습니다.

이번 **서비스 계층(Service Layer)**에서는 비즈니스 로직의 수행 방법과 **저장소(Repository)**에게 데이터를 어떤방식으로 요청하는지 확인해보도록 하겠습니다.

js
// services/posts.service.js

const PostRepository = require('../repositories/posts.repository');

class PostService {
  postRepository = new PostRepository();

  findAllPost = async () => {
    // 저장소(Repository)에게 데이터를 요청합니다.
    const allPost = await this.postRepository.findAllPost();

    // 호출한 Post들을 가장 최신 게시글 부터 정렬합니다.
    // 오름차순
    allPost.sort((a, b) => {
      return b.createdAt - a.createdAt;
    })

    // 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
    return allPost.map(post => {
      return {
        postId: post.postId,
        nickname: post.nickname,
        title: post.title,
        createdAt: post.createdAt,
        updatedAt: post.updatedAt
      }
    });
  }

  createPost = async (nickname, password, title, content) => {
    // 저장소(Repository)에게 데이터를 요청합니다.
    const createPostData = await this.postRepository.createPost(nickname, password, title, content);

    // 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
    return {
      postId: createPostData.null,
      nickname: createPostData.nickname,
      title: createPostData.title,
      content: createPostData.content,
      createdAt: createPostData.createdAt,
      updatedAt: createPostData.updatedAt,
    };
  }
}

module.exports = PostService;

이번 **서비스 계층(Service Layer)**에서는 PostServicePostRepositoryfindAllPost, createPost 메소드를 호출한 것을 확인할 수 있습니다. 해당 코드는 서비스비즈니스 로직을 수행하기 위해 필요한 데이터를 **저장소 계층(Repository Layer)**에게 요청하여 가져오는 것을 확인 할 수 있습니다.

그리고 서비스에서 특별한 점은 return allPost.map(post => {}); 와 같이 데이터를 가공하는 코드인데요, 만약 Repository에서 데이터를 가져와 가공하지 않고 클라이언트에게 전달할 경우 사용자의 Password같이 알아서는 안될 정보까지 전달되게 되어 보안성이 떨어지는 결과를 낳게됩니다.

Express로 구현하는 저장소

  • 저장소 계층(Repository Layer)이란 데이터 엑세스 계층(Data Access Layer)이라고도 불리는데요 대표적으로 Database와 관련된 작업을 수행하는 계층입니다.

  • 모든 데이터Memory상에 존재하는 것처럼 가정해 데이터 접근과 관련된 세부 사항감춥니다

  • 대표적인 저장소 계층의 메소드

    • add() : 새 원소를 저장소에 추가합니다.
    • get() : 이전에 추가한 원소를 저장소에서 가져옵니다.
  • 저장소 계층을 구현했을 때 데이터를 저장하는 방법을 더 쉽게 변경할 수 있고, 테스트 코드 작성시 **가짜 저장소(Mock Repository)**를 제공하기가 더 쉬워집니다.

  • 어플리케이션의 다른 계층에서는 저장소의 세부 사항이 어떤 방식으로 구현되어 있더라도 영향을 받지 않습니다.객체 지향의 개념 중에서 추상화(Abstraction)와 관계가 있습니다.

  • 저장소 계층은 데이터 저장소를 간단히 추상화한 것으로 이 패턴을 사용하면 모델 계층데이터 계층분리할 수 있습니다.

장점
  • 모델인프라에 대한 사항을 완전히 분리했기 때문에 **단위 테스트(Unit test)**를 위한 **가짜 저장소(Fake Repository)**를 쉽게 만들 수 있습니다.
  • 도메인 모델을 미리 작성하면 처리해야 할 비즈니스 문제에 더 잘 집중할 수 있다.
  • 접근 방식을 바꾸고 싶을 때 외래키마이그레이션 등을 염려하지 않고 모델에 반영할 수 있다.
  • 객체테이블매핑하는 과정을 원하는 대로 제어할 수 있어서 DB 스키마단순화할 수 있다.
  • 저장소 계층에 ORM을 사용하면 필요할 때 MySQLPostgres와 같이 DB를 서로 바꾸기 쉬워집니다.
단점
  • 저장소 계층이 없더라도 ORM이 어느 정도 (모델저장소의) 결합을 완화시켜줍니다.

  • ORM 매핑수동으로 하려면 개발 코스트가 더욱 소모됩니다.

    → 여기서 설명하는 ORM은 저희가 이전에 사용한 Sequelize와 같은 라이브러리를 말합니다.

구현

리포지토리

  • DB 관리 (연결, 해제, 자원 관리)
  • DB CRUD 작업 처리

3계층 아키텍처의 마지막 계층인 **저장소 계층(Repository Layer)**입니다!

이전 코드에서 **서비스 계층(Service Layer)**인 PostServices에서 PostRepository를 호출하여 데이터요청하는 것을 확인 할 수 있었는데요, **저장소 계층(Repository Layer)**에서는 데이터베이스데이터를 어떠한 방식으로 가져와 상위 계층에게 반환하는지 확인해보도록 하겠습니다.

js
// repositories/posts.repository.js

const { Posts } = require('../models');

class PostRepository {
  findAllPost = async () => {
    // ORM인 Sequelize에서 Posts 모델의 findAll 메소드를 사용해 데이터를 요청합니다.
    const posts = await Posts.findAll();

    return posts;
  }

  createPost = async (nickname, password, title, content) => {
    // ORM인 Sequelize에서 Posts 모델의 create 메소드를 사용해 데이터를 요청합니다.
    const createPostData = await Posts.create({ nickname, password, title, content });

    return createPostData;
  }
}

module.exports = PostRepository;