본문 바로가기
JavaScript/node.js

아키텍쳐 패턴(Architecture Pattern)

by nacjji 2022. 12. 10.
아키텍쳐 패턴

소프트웨어의 구조를 구성하기 위한 가장 기본적인 토대

각각의 시스템들과 그 역할이 정의 되어 있고, 여러 시스템 사이의 관계와 규칙 등이 포함되어 있다. 

검증된 구조로 개발을 진행하기 때문에 안정적인 개발이 가능하다. 

아키텍쳐 패턴을 도입할 경우 도메인이 복잡할스록 모델이나 코드를 더 쉽게 변경할 수 있다. 

 

대표적인 아키텍쳐 패턴
  • 저장소 패턴 
    • 영속적인 저장소에 대한 추상화 
  • 서비스 계층 패턴
    • 유스 케이스(Usecase)의 시작과 끝을 명확하게 정의하기 위한 패턴
  • 작업 단위 패턴
    • 원자적 연산을 제공
  • 애그리게이트 패턴
    • 데이터 정합성을 강화하기 위한 패턴

 

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

 

계층형 아키텍쳐 패턴

계층을 분리해서 관리하는 아키텍쳐 패턴으로, 현재 가장 흔하게 사용하는 아키텍쳐 패턴이다. 

단순하고 대중적이며 비용도 적게 들어 모든 애플리케이션의 사실상 표준 아키텍쳐이다. 

계층형 아키텍쳐 패턴은 어떤 경우든 게층을 분리해서 유지하고, 각 계층이 자신의 바로 아래 계층에만 의존하게 만드는 것이 목표이고,  각 계층의 응집도가 높으면서 다른 계층과는 낮은 결합도를 가지고 있어야 한다. 

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

 

3계층 아키텍쳐
  • 프레젠테이션 계층 
  • 비즈니스 로직 계층 
    • 실제로 비즈니스 로직이 구현되는 계층
  • 데이터 엑세스 계층
    • 실제 데이터베이스와 연결되어 데이터베이스의 데이터를 가져와 사용하는 계층

persistance Layer는 데이터 엑세스 계층이다.

  • Controller : 어플리케이션의 가장 바깥 부분, 요청, 응답을 처리한다. 
  • Service : 어플리케이션의 중간 부분, 아키텍쳐의 가장 핵심적인 비즈니스 로직이 수행되는 부분이다.
  • Repository : 어플리케이션의 가장 안쪽 부분, DB와 맞닿아 있다. 실제 데이터베이스의 데이터를 사용하는 계층이다. 

 

3계층 아키텍쳐 흐름도

  • 클라이언트가 요청을 보낸다. 
  • 요청을 URL 에 알맞은 컨트롤러가 수신받는다. 
  • 컨트롤러는 넘어온 요청을 처리하기 위해 서비스를 호출한다. 
  • 서비스는 필요한 데이터를 가져오기 위해 저장소에게 데이터를 요청한다. 
  • 서비스는 저장소에서 가져온 데이터를 가공하여 컨트롤러에게 전달한다. 
  • 컨트롤러는 서비스의 결과물을 클라이언트에게 전달한다. 

 

 

계층형 아키텍쳐 패턴의 장점

관심사를 분리하여 현재 구현하려는 코드를 명확히 인지할 수 있다. 

각 계층별로 의존성이 낮아 모듈을 교체하더라도 코드 수정이 용이하다. 

계층별로 테스트를 작성할 수 있어 테스트 코드를 좀 더 용이하게 구성할 수 있다. 

 

컨트롤러

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

클라이언트의 요청을 수신하고, 요청에서 들어온 데이터 및 내용을 검증한 뒤 서버에서 수행된 결과를 클라이언트에 반환한다. 

 

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

프레젠테이션 계층은 대표적으로 컨트롤러로 사용된다. 사용자가 서버에 요청을 하게 되면 가장 먼저 만나게 되는 계층이다. 

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

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

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

 

Express 에서의 컨트롤러

클라이언트의 요청을 받음

요청에 대한 처리는 서비스에 전달

클라이언트에게 응답

Express로 구현하는 컨트롤러

// Controller 에서는 특정 요구사항을 서비스 계층에게 단순한 호출을 하고 컨트롤러에서 받은 결괏값을 사용자에게 보여주는 역할을 한다.

// 서비스 계층에 post 에 해당하는 서비스 모듈을 가져와서 PostService 변수에 할당한다.
const PostService = require("../services/posts.service")

// Post 의 컨트롤러 역할을 하는 클래스
class PostsController {
  // Post 서비스의 클래스를 컨트롤러 클래스의 멤버 변수로 할당, 인스턴스로 생성
  postService = new PostService()

  getPosts = async (req, res, next) => {
    // 서비스 계층에 구현된 findAllPost 로직 실행
    // http 메소드 get, 기본 url 일 때 실행
    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 로직 실행
    // http 메소드 post, 기본 url 일 때 실행
    const createPostData = await this.postService.createPost(nickname, password, content)

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

module.exports = PostsController

 

서비스 계층

비즈니스 로직 계층이라고도 불리며, 아키텍쳐의 가장 핵심적인 비즈니스 로직을 수행하고, 실제 사용자가 원하는 요구사항을 구현하는 계층이다. 

프레젠테이션 계층과 데이터 엑세스 계층 사이에서 중간 다리 역할을 하며 두 계층이 직접 통신하지 않게 만들어준다. 

서비스는 데이터가 필요할 때 저장소에게 데이터를 요청한다. 

어플리케이션의 규모가 커질 수록 역할 및 코드 또한 커지게 되며, 어플리케이션의 핵심적인 비즈니스 로직을 수행하여 클라이언트의 요구사항을 반영하여 원하는 결과를 반환해주는 계층이다. 

 

서비스 계층의 장단점
  • 장점
    • 각각의 유스 케이스와 워크플로우를 명확히 정의할 때 도움이 된다. 
    • 저장소(Repository)에게 얻을 필요가 있는 데이터가 무엇인지 이해할 수 있다. 
    • 어떤 사전 검사와 현재 상태 검증을 필수적으로 해야하는 것인지 이해할 수 있다. 
    • 어떤 내용을 저장해야 하는지 이해할 수 있다. 
    • 비즈니스 로직을 API 뒤에 감췄기 때문에 서비스 계층의 코드를 자유롭게 리팩토링 할 수 있다. 
    • 저장소 패턴 및 가짜 저장소와 조합하면 높은 수준의 테스트를 작성할 수 있다. 
  • 단점
    • 서비스 계층 또한 다른 추상화 계층에 불과하다. 
    • 너무 많은 기능을 넣으면 빈약한 도메인 모델과 같은 안티 패턴이 생길 수 있다. 
    • 서비스 계층 또한 여러 가지로 분리를 해서 구현하는 것이 좋다.

 

Express로 구현하는 서비스 계층

// Service 계층에선 실제로 데이터를 가공하는 비즈니스 로직을 수행한다.
// 데이터 베이스 내에 접근하기 위해 Repository를 호출해야 한다.
const PostRepository = require("../repositories/posts.repository")

class PostService {
  // PostRepository 인스턴스 생성
  postRepository = new PostRepository()

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

    // 호출한 Post들을 가장 최신 게시글 부터 정렬한다.
    allPost.sort((a, b) => 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

 

저장소(Repository) 계층

데이터 엑세스 계층으로 대표적으로 데이터베이스와 관련된 작업을 수행하는 계층이다. 

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

저장소 계층을 구현했을 때 데이터를 저장하는 방법을 더 쉽게 변경할 수 있고 테스트 코드 작성 시 가짜 저장소를 제공하기가 더 쉬워진다. 

어플리케이션의 다른 계층에서는 저장소의 세부사항이 어떤 방식으로 구현되어 있더라도 영향을 받지 않는다. 

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

 

대표적인 저장소 계층의 메소드
  • add() : 새 원소를 저장소에 추가한다. 
  • get() : 이전에 추가한 원소를 저장소에서 가져온다. 

 

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

// Sequelize 모델 Posts를 가져온다.
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

'JavaScript > node.js' 카테고리의 다른 글

Sequelize paranoid(삭제-복구)  (0) 2022.12.24
컨트롤러(Controller)  (0) 2022.12.10
JWT  (0) 2022.12.08
Node.js, 파일 시스템 기능  (1) 2022.11.24
Node.js, node.j란?  (0) 2022.11.24